diff --git a/design/engineering/test-conventions.md b/design/engineering/test-conventions.md index e4a7e133..5ca30a57 100644 --- a/design/engineering/test-conventions.md +++ b/design/engineering/test-conventions.md @@ -1,7 +1,7 @@ # 测试规范(Dungeon Sweeper) > 约束范围:`game/probe/ 与 game/sim/`、`game/tests/` 与一切场景探针的编写与验收。 -> 分工:测试**执行许可**(什么时候允许跑测试)归 AGENTS.md「硬性红线·测试执行两入口铁规」;**跑什么闸、什么顺序**归 `tools/run_all_gates.sh`(脚本即清单,本文与 AGENTS.md 均不另列);本文管测试怎么写、怎么算合格。 +> 分工:测试**执行许可**(什么时候允许跑测试)归 AGENTS.md「硬性红线·测试执行两入口铁规」;**跑什么闸、什么顺序**归 `tools/run_all_gates.sh`(脚本即清单,本文与 AGENTS.md 均不另列);本文管测试怎么写、怎么算合格。每件测试工具**是什么、判什么、怎么单独跑、什么时候适合单独用**——见 `game/tests/README.md`(测试工具手册),本文不重复工具介绍。 **写任何测试前必须通读本文。** @@ -102,6 +102,7 @@ - **修改既有行为**:只撤销该改动(回退该 hunk 或该处接线)→ 跑能覆盖它的最窄测试 → 对应断言必须变红 → 还原 → 复绿。 - **新增行为**:只撤销该新增(回退该 hunk 或删去该新增段)→ 跑能覆盖它的最窄测试 → 对应断言必须变红 → 还原 → 复绿。 - **撤销后仍然绿**:说明断言没钉在这个改动上,判缺漏;不得用「代码能跑通」代替。 +- **判据绑可观测提交面**:端到端断言的判据要绑「被测改动特有的可观测提交面」(信号发射、事件、入参、绘制记录),不绑多层防御之后的终态——否则改动被下游兜底吸收时终态不变、断言假绿。例:探针「雾不扩可点域」首版只看屏/选路态终态,命中面放宽后断言仍绿;改绑 `route_chosen` 发射面后即红。 - 每张任务卡至少一条撤销证明记录;持久化、生命周期、存档、跨战共享这类接线逐行做。 ### 8.5 禁止削弱测试 diff --git a/design/gdd/systems/冒险循环/关卡节点图生成.md b/design/gdd/systems/冒险循环/关卡节点图生成.md index fc702680..d56e0be4 100644 --- a/design/gdd/systems/冒险循环/关卡节点图生成.md +++ b/design/gdd/systems/冒险循环/关卡节点图生成.md @@ -1,7 +1,7 @@ # 关卡节点图生成 > 一句话:一关的节点图从哪来——生成器按参数长出一张合法图 -> 状态:现行为 v1.1 +> 状态:现行为 v1.3 ## 设计目的 @@ -38,7 +38,7 @@ ### 图的规格:什么算一张合法的图 -合法图是一张岔路发散式道路系统:主路+支路,宽度由分叉自然长出,多终点时提前承诺归属。产出约束五条,每张产出图都必须满足: +合法图是一张岔路发散式道路系统:主路+支路,宽度由分叉自然长出,多终点时提前承诺归属。产出约束六条,每张产出图都必须满足: | 约束 | 内容 | |---|---| @@ -47,6 +47,9 @@ | 层高=沿路步数 | 入口到终点的最长路径步数;第一关 ≥6,逐关递增且增幅变大 | | 出口归属锁定 | 多终点时由大岔口把图分成互不连达的分支区域,各区域只通向自己的终点;每个终点前一层节点的去路集合恰含一个终点——选路=提前承诺打谁 | | 战斗节点保底 | 奖励/恢复/商店占位后战斗节点量不被挤没(保底数进调参表) | +| 多终点深度互异 | 多终点(多 Boss 节点)必须落在互异的节点深度;同一节点深度出现两个终点即判产出无效——共深会让「走哪条路打谁」落空 | + +节点深度的算法:自入口沿路走到该节点的最长路径步数;多入口时取距任一入口的最长路径。不用最短路径——合流节点或多入口图上取最短,会让某些出边指向深度不增甚至更浅的节点,破坏「连线只朝前进方向」的无环判据。 结构铁律:连线只朝前进方向(无环);入口、终点都可多个(个数=参数档),每条路径恰好从一个入口出发、恰好到达一个终点。 @@ -75,13 +78,14 @@ 配比是生成旋钮:战斗/奖励/恢复/商店各占多少由参数档定,生成器掷出分布落在图上成为节点类型;配比只管非终点节点,终点恒为 Boss、不进配比。节点类型在可见范围内对玩家公开(可见性规则归关卡节点地图)——看得见的类型才谈得上提前规划。 -摆放约束(三条): +摆放约束(四条): | 约束 | 内容 | |---|---| | 商店位 | 每个终点前一位恰一家商店——每名玩家一局必经恰一家店 | -| 岔路取舍·下一跳 | 一个岔口分出的各条去路,下一节点类型互不相同——下一跳必在可见范围,是决策的可见锚 | +| 岔路取舍·下一跳 | 一个岔口分出的各条去路,下一节点类型互不相同——下一跳必在可见范围,是决策的可见锚(实现侧已兑现:候选集强制豁免类型可见性) | | 岔路取舍·支路构成 | 一个岔口分出的各条支路,整条支路的节点类型构成互不相同——每条支路至少有一个别的支路没有的类型(例:一路有恢复节点、一路有奖励节点) | +| 支路互异上限=出口数 3 | 商店与 Boss 每条支路必有,可供支路互斥的类型只剩战斗/奖励/恢复三类——一条链上的岔口最多切出 3 条互异支路,故出口个数取值域为 1–3(见生成参数旋钮表) | 内容归各 owner:战斗节点打谁=敌人系统按深度带配池;奖励/恢复/商店的兑付语义=关卡节点地图与各 owner 文档。 @@ -113,12 +117,13 @@ | 配比 | 四类占比合计 1,战斗保底优先 | 攻击性/兑付/恢复的节奏配比 | provisional | | 战斗节点保底数 | ≥ 调参值 | 成长位不挤没战斗——战斗是本作核心量 | provisional | | 商店数 | =出口数 | 每局决战前恰一次补给,成长保底是结构事实 | 已锁 | +| 图包络 | ≤ 屏预算(1280×800 下约 10 层×8 槽) | 超预算只许限参数档,禁改画布冻结公式 | 已锁 | ## 验收标准 | 判据 | 内容 | |---|---| -| 产出合格 | 任意参数档产出的图必过产出约束五条+结构铁律;任意入口出发的任意路径都恰好到达一个终点 | +| 产出合格 | 任意参数档产出的图必过产出约束六条+结构铁律;任意入口出发的任意路径都恰好到达一个终点 | | 生长递增 | L1/L2/L3 读数:最长路径步数与节点总数均逐关递增;节点总数增幅 Δ(L2→L3)>Δ(L1→L2) | | 归属锁定 | 多出口图每个终点前一层节点的去路集合恰含一个终点;各分支区域互不连达 | | 必经恰一店 | 任意入口→终点路径经过且恰经过一家店 | diff --git a/design/gdd/systems/冒险循环/关卡节点地图.md b/design/gdd/systems/冒险循环/关卡节点地图.md index f716fc77..2e9de0e0 100644 --- a/design/gdd/systems/冒险循环/关卡节点地图.md +++ b/design/gdd/systems/冒险循环/关卡节点地图.md @@ -1,7 +1,7 @@ # 关卡节点地图 > 一句话:单关内的节点图选路与推进 -> 状态:现行为 v2.1 +> 状态:现行为 v2.3 ## 设计目的 @@ -54,7 +54,7 @@ | 节点类型 | 机制承诺 | |---|---| -| 战斗 | 携带节点编号;行进至此开战(兑付见下节);胜利后回到选路 | +| 战斗 | 携带节点深度;行进至此开战(兑付见下节);胜利后回到选路 | | Boss | 恒为图终点;行进至此开 Boss 战;胜利即关卡收束点 | | 奖励 | 行进至此触发节点奖励(兑付见下节);反馈呈现后回到选路 | | 恢复 | 行进至此向冒险结构发治疗封顶回报;反馈呈现后回到选路 | @@ -62,9 +62,9 @@ 商店房的编排规则:**每个终点前一位恰一家店**——哈迪斯式的"决战前最后一个补给点":买到的属性还有整段战斗可消费,错过的成长也没有第二家店可补。终点前一位同时是**必经位**:每名玩家一局恰好必经一家店,成长保底是图结构事实而非玩家自觉。这条位置约束不由加载校验承载,由冒烟断言把关(摆放规则语义见[关卡节点图生成](关卡节点图生成.md)配比节)。 -每个节点在图上有**节点深度**(本文含义:从入口沿路走过来的步数),用来当战斗的定位地址——遭遇坐标=关卡序号+节点深度,敌人表按**深度带**(深度区间)配池,寻址语义见[敌人系统](../战斗循环/敌人系统.md)。"最深处是关底"的直感由深度承载:终点恒在各路径最深处。单链(每步只有一条连线)是合法退化形态,节点深度等于它前面走过的战斗场数;三关参数档都含岔口,第一关不再是单链。图的来源见[关卡节点图生成](关卡节点图生成.md)。 +每个节点在图上有**节点深度**(本文含义:自入口沿路走到该节点的最长路径步数;多入口时取距任一入口的最长路径),用来当战斗的定位地址——遭遇坐标=关卡序号+节点深度,敌人表按**深度带**(深度区间)配池,寻址语义见[敌人系统](../战斗循环/敌人系统.md)。不用最短路径:合流节点或多入口图上取最短,会让某些出边指向深度不增甚至更浅的节点,破坏「连线只朝前进方向」的无环判据。"最深处是关底"的直感由深度承载:终点恒在各路径最深处。单链(每步只有一条连线)是合法退化形态,节点深度等于它前面走过的战斗场数;三关参数档都含岔口,第一关不再是单链。图的来源见[关卡节点图生成](关卡节点图生成.md)。 -呈现侧口径(2026-09-17 用户拍板):节点图的画面表达走**方块地牢观感**——节点画成地牢房间的方块、连线画成通道——但逻辑拓扑就是本文的节点图,不产生任何额外选路或探索语义。关卡画面本体即节点链进度的呈现载体(选路覆盖层的常驻形态,节点态直接读图:已过暗金 / 当前金 / 候选下一跳通道亮金 / Boss 封印黑描边(旗朱→封印紫→封印黑 #000000,2026-09-28 用户拍板现行)),不另设进度条组件;视觉规格归 [HUD 框架](../表现与支撑/HUD框架.md)。 +呈现侧口径(2026-09-17 用户拍板):节点图的画面表达走**方块地牢观感**——节点画成地牢房间的方块、连线画成通道——但逻辑拓扑就是本文的节点图,不产生任何额外选路或探索语义。关卡画面本体即节点链进度的呈现载体(选路覆盖层的常驻形态,节点态直接读图:已过暗金 / 当前金 / 候选下一跳通道亮金 / Boss 紫赤描边(紫赤+深墨环,视觉定稿见 [level-map](../../ux/level-map.md))),不另设进度条组件;视觉规格归 [HUD 框架](../表现与支撑/HUD框架.md)。 **商店覆盖层呈现**(呈现 spec:[design/gdd/ux/shop.md](../../ux/shop.md) v1.0,2026-09-30;**v1.5 注记**——pool 模式货架面=共享池三选一同构卡〔进店抽 3/买 1 全重抽/空架售罄态〕,与三选一奖励同一交互语言;legacy 固定三件面随对照模式保留。**v1.5-a 注记(2026-10-04 用户裁)**:~~库存余量就近~~ **库存余量不呈现**(货架卡不带「库存余 N/∞」字样——库存语义仍在共享池令牌库,仅呈现退役;独立刷新机制另见待办清单「商店独立刷新机制」条):兑付 = 打开商店覆盖层(模态)——货架卡陈列、GP 余额读数、购买/离开;离开后兑付确认回选路。本节锁编排面(何时开/何时关/回选路),呈现细节归 spec;定价数值归[难度曲线与Boss](难度曲线与Boss.md)(shop.tres)。 @@ -102,6 +102,8 @@ Boss 兑付(胜利或英雄死亡)触发关卡收束点,此后不再有选 终点恒为 Boss,**Boss 标记与终点身份不入雾**——「走哪条=打谁」是提前承诺,承诺必须有信息前提:雾遮的是途中有什么,不遮终局打谁。N 为本系统自有参数(见调参表)。 +实现语义三句:层距=从入口出发的 BFS 层差;Boss(含终点)与当前候选节点双豁免,恒显类型;可见性只算当前这一圈——已走过的节点回雾,不累积记忆。 + ### 节点兑付:把控制流移交给小循环 **人话阶段**:节点兑付就是"走到这个房间里,这个房间的事开始发生"。本文按节点类型触发兑付,然后把属于别家领域的事交还给别家——本文自始至终不生产任何数值。 @@ -143,7 +145,7 @@ Boss 兑付(胜利或英雄死亡)触发关卡收束点,此后不再有选 |---|---|---| | 上游 / 下游 | [冒险结构](冒险结构.md) | 接口契约四件套(开始关卡 / 战斗结果上报 / 治疗封顶回报 / 关卡结束)+ 收束点次序语义:向下接开始关卡,向上报战斗结果、恢复请求与关卡结束 | | 上游 | [奇物](奇物.md) | 节点奖励件的分类、凭证与持有语义(提升池构成与共享池库存归[冒险结构](冒险结构.md)) | -| 上游 | [敌人系统](../战斗循环/敌人系统.md) | 定位信息的消费方:本文递出关卡序号 + 节点编号,按编号生成单位归它 | +| 上游 | [敌人系统](../战斗循环/敌人系统.md) | 定位信息的消费方:本文递出关卡序号+节点深度,敌人侧按深度带配池生成单位 | | 上游 | [难度曲线与Boss](难度曲线与Boss.md) | 敌我数值曲线与遭遇投放节奏的调参所有权;本文零数值 | | 上游 | [数据配置](../表现与支撑/数据配置.md) | 商店表(商品行 / GP 掉落参数 / 价格参数)的承载与统一读取;商店覆盖层的商品与定价数据源 | | 下游 | [战斗结算](../战斗循环/战斗结算.md) | 两个调用入口的发起方:战斗发起组装(定位信息)、关卡收束(清库) | @@ -153,7 +155,7 @@ Boss 兑付(胜利或英雄死亡)触发关卡收束点,此后不再有选 1. 从冒险结构接收开始关卡(关卡序号、当前生命、生效上限),加载本关节点图并呈现选路停点(关卡画面),玩家在图上选出第一跳,行进自所选节点开始(入口节点不自动进入)。 2. 每节点兑付完成并确认后返回关卡画面,玩家再选路前进;岔路选一条、单链点继续;零自动推进。 -3. 战斗 / Boss 节点:组装关卡序号 + 节点编号,移交战斗结算开战;战斗结束向冒险结构上报战斗结果。 +3. 战斗 / Boss 节点:组装关卡序号 + 节点深度,移交战斗结算开战;战斗结束向冒险结构上报战斗结果。 4. 奖励节点:触发节点奖励兑付 = 提升三选一(与通关三选一共享令牌库);恢复节点:向冒险结构发治疗封顶回报;商店节点:打开商店覆盖层,购买即时生效,离开后兑付确认。 5. Boss 战胜利或任意战斗倒下 = 关卡收束点:先调战斗结算的关卡收束清库,再宣布关卡结束。 6. 单链是合法退化形态;三关参数档都含岔口,第一关即有选择(图的来源见[关卡节点图生成](关卡节点图生成.md))。 @@ -169,13 +171,13 @@ Boss 兑付(胜利或英雄死亡)触发关卡收束点,此后不再有选 ## 调参表 -本系统自有参数一项——**信息可见层数 N**(int ≥ 1,provisional,定值批落数据表):节点类型可见距离(层),范围外信息迷雾只见结构。图结构合法性是规则不是参数(规则语义见[关卡节点图生成](关卡节点图生成.md)的产出规格),由加载校验强制;图的来源与节点构成归[关卡节点图生成](关卡节点图生成.md),战斗数值归难度曲线与Boss。 +本系统自有参数一项——**信息可见层数 N**(int ≥ 1,provisional,已落数据表:`level.tres` 的 `info_visible_layers`,现行值 2):节点类型可见距离(层),范围外信息迷雾只见结构。图结构合法性是规则不是参数(规则语义见[关卡节点图生成](关卡节点图生成.md)的产出规格),由加载校验强制;图的来源与节点构成归[关卡节点图生成](关卡节点图生成.md),战斗数值归难度曲线与Boss。 ## 验收标准 - 图合法:行集通过加载校验——每条路径恰好从一个入口出发、恰好到达一个终点,终点恒为 Boss(可多个)、连线无环、任意入口出发的任意路径都到达某一个终点;违规配置加载期致命(规则语义见[关卡节点图生成](关卡节点图生成.md)产出规格)。 - 行进自主:从入口到 Boss 的每一次前进都由玩家选路发起(含关卡开始后的第一跳——关卡开始停在图上,入口节点不自动进入);不存在任何自动推进路径;倒下时行进终止。 -- 兑付移交:每场开战调用携带(关卡序号 + 节点编号),与节点图上该节点位置一致;每场战斗结束恰一次战斗结果上报。 +- 兑付移交:每场开战调用携带(关卡序号 + 节点深度),与节点图上该节点位置一致;每场战斗结束恰一次战斗结果上报。 - 兑付后确认:战斗节点的继续确认挂战斗总结结算(settlement-summary.md),奖励/恢复挂兑付反馈覆盖层;到达与兑付之间无确认步。 - 恢复兑付:治疗封顶回报载荷 = 该关恢复比例(读关卡配置行);满血回复 0 是合法状态(执行语义归冒险结构)。 - 奖励兑付:奖励节点兑付恰触发一次;池空直通空态合法,现行 = 提升三选一(与通关三选一共享令牌库)取一件按件类兑付(spec:node-reward.md v2.x;兑付路由归[冒险结构](冒险结构.md)提升池节)。 diff --git a/design/gdd/systems/冒险循环/冒险结构.md b/design/gdd/systems/冒险循环/冒险结构.md index 890add6b..e95d792f 100644 --- a/design/gdd/systems/冒险循环/冒险结构.md +++ b/design/gdd/systems/冒险循环/冒险结构.md @@ -261,7 +261,7 @@ GP(显示名「GP」)是冒险内的一次性属性提升货币:战斗胜 | 参数 | 域 | 现行值 | 说明 | |---|---|---|---| -| 关卡行集(关数与图的参数档) | 关卡表逐关行 | 三关现行(图由生成器按参数档产出,见[关卡节点图生成](关卡节点图生成.md);旧模板池随实现批清数据) | 加关 = 内容批(参数档 + 敌我数值 + 新元素同批);确定性 = 固定种子+固定参数档(首局教学与存档复现的固定图由此承载,无兜底模板) | +| 关卡行集(关数与图的参数档) | 关卡表逐关行 | 三关现行(图由生成器按参数档产出,见[关卡节点图生成](关卡节点图生成.md)) | 加关 = 内容批(参数档 + 敌我数值 + 新元素同批);确定性 = 固定种子+固定参数档(首局教学与存档复现的固定图由此承载,无兜底模板) | | 恢复比例 | int [0,100],逐关行 | 见 level.tres rest_heal_pct(provisional) | 恢复点回复生效上限的百分比;随上限加成同步放大;100 = 恢复点变全满泉,0 = 死节点 | | 上限提升 | int [0,999],冒险表逐件列 | adventure_table bonus_attack_flat / bonus_max_hp_flat / bonus_armor_flat(provisional) | 三选一属性件各自兑付值;值锚与上调约束归难度曲线与Boss | | 三选一件值(其余) | int [0,999],冒险表 kind×tier 列族 | adventure_table curio_value_*(GP 袋 / AP 件 / 恢复 / 刷新,provisional) | 令牌件值读表;道具件授予件数随秘宝件出池退役 | @@ -290,7 +290,7 @@ GP(显示名「GP」)是冒险内的一次性属性提升货币:战斗胜 - 同调位约束:道具终态持有恒 ≤ 同调位(谓词表实时求值);超位时停点渠道触发替换交互(丢旧收新 / 放弃新件两路),战斗内渠道沉没不入;奖励节点渠道同受约束。(现行零道具件投放——约束保留作契约,见「秘宝件受同调位约束」节注。) - 钥石逐杀:Boss 击杀后钥石累计增量 = boss_keystone 行值(首杀/复杀两档);大奖励面钥石入账零残留。 - 收束次序:通关宣布到达时库存已被关卡节点地图先行清空;重复的关卡结束宣布被拒绝,状态与摘要零变化。 -- 覆盖义务:冒烟断言——行集内每个战斗与关底节点(关、深)在敌人与棋盘生成的数值表中有精确行;序列关数 = 行集关数。 +- 覆盖义务:冒烟断言——行集内每个战斗与关底节点(关、深)都必须在敌人与棋盘生成的数值表里命中:敌侧按深度带命中(落带不早于请求深度,禁以顶带 clamp 消解),板侧按解析命中;生成图深度域 L1 0–6/L2 0–9/L3 0–13(参数档 `layers` 实测,provisional);序列关数 = 行集关数。 - 生效前提未兑现(第二关行缺行):以单关冒险运行(合法态),越界关断言显式跳过而非失败。 - 放弃:战斗中的放弃请求被拒绝;非战斗期放弃按失败同型结算,已获大奖励不追回;放弃入口仅在选路停点呈现且只在冒险模式开放。 - 种子域:冒险内零随机流自开,一切随机派生自主种子(含 GP 掉落掷定的 gp_drop 封闭流);同种子同操作序列两局逐位可复现。 diff --git a/design/gdd/systems/战斗循环/敌人系统.md b/design/gdd/systems/战斗循环/敌人系统.md index 231b4fdf..72f32035 100644 --- a/design/gdd/systems/战斗循环/敌人系统.md +++ b/design/gdd/systems/战斗循环/敌人系统.md @@ -64,21 +64,21 @@ ### 开战生成:主角数据行与一场 1–4 个敌人(行集可混编多种敌种) -战斗开始时,编排方给出遭遇坐标(关卡序号 + 深度带——节点深度所在的区间),本系统据此在敌人表里取出该坐标的整个行集(一行或多行),把本战实际数量的敌人逐行生成出来——每行各按自己的数量行值实例化(单行 1 到 3 个),行集实例总数(各行开战骰后数量求和)不超过 4。同坐标多行即混编遭遇:各敌种同场上阵,生成序 = 表文件行序(行序即契约——目标顺位、摇号子流派生、开战叙述取首行都按它,加载期校验兜底:同坐标多行必须文件内相邻)。单行行集 = 既有形态:一场 1 到 3 个同种同属性敌人,来自同一行表值。战斗中敌人只减不增,不做召唤、增援、分裂。主角同时以当前血量入参生成,血量越界(高于上限或不超过 0)整单拒绝、不留半实例。 +战斗开始时,编排方给出遭遇坐标(关卡序号 + 深度带——节点深度所在的区间),本系统据此在敌人表里取出该坐标的整个行集(一行或多行),把本战实际数量的敌人逐行生成出来——每行各按自己的数量行值实例化(单行 1 到 3 个),行集实例总数(各行开战骰后数量求和)不超过 4。同带键多行即混编遭遇:各敌种同场上阵,生成序 = 表文件行序(行序即契约——目标顺位、摇号子流派生、开战叙述取首行都按它,加载期校验兜底:同带键多行必须文件内相邻)。单行行集 = 既有形态:一场 1 到 3 个同种同属性敌人,来自同一行表值。战斗中敌人只减不增,不做召唤、增援、分裂。主角同时以当前血量入参生成,血量越界(高于上限或不超过 0)整单拒绝、不留半实例。 生成拿到的是当时的表值快照:开战那一刻把每行的数值和池模板各抄一份,之后改表不影响已生成的单位。池模板快照成每实例自己的实例池(各技能冷却清零),此后各自独立摇号、互不干涉。某行的数量或攻击基线若配成了区间,由战斗结算用封闭随机流在开战时**按行各掷定一次**(每行掷自己的区间;掷定次数随行数——单行行集的掷定次数与既有口径逐位一致,混编不新增随机源形态)后传入确定值——本系统自身始终零随机;掷定后整场持久,攻击基线不逐拍重掷(数字要可读)。 | 规则 | 含义 | |---|---| | 敌方效果对全体生效 | 范围效果作用于所有存活敌人——混编放开后(2026-10-07 行集多行批)场上可多种敌种共存,结算语义按存活单位逐一生效、不依赖"场上全同种"前提;"只打某一个"的单体寻址暂不开放(见 P1) | -| 行集构成纪律 | 敌人表按(关卡序号 × 深度带)存放;深度带=节点深度的区间——逐局生成使单点深度不再固定,配池按带不按点,难度坡随带递增。同一坐标行集允许多行、允许不同敌种混编;Boss 行按终点配置,多终点各配一行(走哪条路决定打谁)。纪律三条,违规加载期返回错误并止动:①行集内 enemy_id 唯一(同一敌种在同行集仍只许一行);②同坐标多行必须表文件内相邻,行序 = 声明序 = 生成序(行序即契约);③行集实例总数(各行骰后数量求和)≤ 4,单行数量域 [1,3] 不变(UNIT_COUNT_MAX 语义 = 单种数)。行数不设上限;同关随深度换敌种合法 | +| 行集构成纪律 | 敌人表按(关卡序号 × 深度带)存放;深度带=节点深度的区间——逐局生成使单点深度不再固定,配池按带不按点,难度坡随带递增。同一带键行集允许多行、允许不同敌种混编;Boss 行按终点配置,多终点各配一行(走哪条路决定打谁)。纪律三条,违规加载期返回错误并止动:①行集内 enemy_id 唯一(同一敌种在同行集仍只许一行);②同带键多行必须表文件内相邻,行序 = 声明序 = 生成序(行序即契约);③行集实例总数(各行骰后数量求和)≤ 4,单行数量域 [1,3] 不变(UNIT_COUNT_MAX 语义 = 单种数)。行数不设上限;同关随深度换敌种合法 | | 主角数据行 | 主角战斗数值单独放一张全局单行表:生命上限、基础攻击力、护甲等级三字段(见调参表) | | 基础攻击恒存在 | 基础攻击不许配 0(配置为 0 加载期拒绝)——零基线乘任何百分比增益恒为零,"胜利路径恒有"的保障从数据层杜绝死锁 | | 先手攻击不是敌方技能拍 | 每次结算战斗段主角先手攻击顺位最前的存活敌人,这是固定动作;奖励侧的道具与增益是基础攻击之外的规划面伤害,归[地图战斗资源](地图战斗资源.md) | 首发敌种三种——地精、熊地精、食人魔酋长(末者为首发关底 Boss);L2/L3 内容批扩至现行八种。名字是玩家可见专名,池权重与护甲等级行值一律读配置表,本表只存敌种语义模板: -> **hobgoblin 缺口登记(2026-10-13 迭代 24 反哺④)**:敌人表有 `hobgoblin` 数据行((1,3),即第一关第四节点的「大地精」行),**无立绘、不在本敌种表**——登记在案的已知缺口(敌种语义模板未成文故呈现面无定义可依),本批未动;补齐归敌种内容批(立绘+语义模板+敌种表行三件套)。 +> **hobgoblin 缺口登记(2026-10-13 迭代 24 反哺④)**:敌人表有 `hobgoblin` 数据行(寻址键 (1, battle, 3) 的「大地精」行),**无立绘、不在本敌种表**——登记在案的已知缺口(敌种语义模板未成文故呈现面无定义可依),本批未动;补齐归敌种内容批(立绘+语义模板+敌种表行三件套)。 | 敌种 | 技能池构成(权重读表) | 护甲类型 | 护甲等级 | 定位 | |---|---|---|---|---| @@ -262,7 +262,7 @@ Boss 专属身份 = 专属池承载:独占技能 + 权重轮廓(**技能错 各敌种行值、池权重与池 EV 读数(含闸长期速率实测)一律读配置表与调参日志,本文不登记具体数值;EV 重锚与权重投值归[难度曲线与Boss](../冒险循环/难度曲线与Boss.md)。 -结构冻结(非调参项,修订走本文流程):技能类型枚举封闭、每池恰一普攻、每池增益 ≤1、死亡即不动手(摇定后兑现前死亡作废不进账)、每结算每存活敌恰行动一次、行序、先作用后推进、固定顺位、**行集构成纪律(2026-10-07 行集多行批换代——原「每坐标恰一敌种」退役:行集内 enemy_id 唯一 + 同坐标多行文件内相邻且行序 = 声明序 = 生成序 + 行集实例总数 ≤ 4)**、零随机(本系统自身语义零随机不变——摇号与掷点随机一律在[战斗结算](战斗结算.md)结算换算层按封闭随机流掷定,可回放契约 = 同种子同输入逐位复现,单敌出招轨迹可独立复放)。联动纪律:技能倍率 × 攻击(伤害拍支配)、权重 × 冷却 × 保底(池节奏三联动)禁止独立调参,联调须重跑方向性预检并同步 registry(登记 [待办清单](../../../../待办清单.md))。 +结构冻结(非调参项,修订走本文流程):技能类型枚举封闭、每池恰一普攻、每池增益 ≤1、死亡即不动手(摇定后兑现前死亡作废不进账)、每结算每存活敌恰行动一次、行序、先作用后推进、固定顺位、**行集构成纪律(2026-10-07 行集多行批换代——原「每坐标恰一敌种」退役:行集内 enemy_id 唯一 + 同带键多行文件内相邻且行序 = 声明序 = 生成序 + 行集实例总数 ≤ 4)**、零随机(本系统自身语义零随机不变——摇号与掷点随机一律在[战斗结算](战斗结算.md)结算换算层按封闭随机流掷定,可回放契约 = 同种子同输入逐位复现,单敌出招轨迹可独立复放)。联动纪律:技能倍率 × 攻击(伤害拍支配)、权重 × 冷却 × 保底(池节奏三联动)禁止独立调参,联调须重跑方向性预检并同步 registry(登记 [待办清单](../../../../待办清单.md))。 ## 验收标准 @@ -275,7 +275,7 @@ Boss 专属身份 = 专属池承载:独占技能 + 权重轮廓(**技能错 - 警觉授予:踩 N 颗雷叠 N 层、逐层直授无上限;敌方全体共享同一状态值(side-wide)。 - 目标顺位:多敌时单体目标恒为生成序最前存活者,杀最前后顺延;杀至空返回空;单敌时顺位与范围效果重合。 - 快照抗改表:实例化后修改敌人表,已生成单位的数值投影与实例池逐字不变;新实例才用新表值。 -- 表校验:越域值、池权重 ≤ 0、空池、普攻配倍率/冷却、池增益 > 1、skill_id 悬空、行集构成违例(行集内同敌种第二行 / 同坐标多行不相邻)均在加载期返回错误并止动;域内合法极端(攻击基线 0)不拒;主角表恰一行、攻击配 0 拒绝。 +- 表校验:越域值、池权重 ≤ 0、空池、普攻配倍率/冷却、池增益 > 1、skill_id 悬空、行集构成违例(行集内同敌种第二行 / 同带键多行不相邻)均在加载期返回错误并止动;域内合法极端(攻击基线 0)不拒;主角表恰一行、攻击配 0 拒绝。 - 零随机回放:同一动作与受击序列驱动两个独立实例,存活投影、逐实例摇定轨迹、顺位返回序列逐项相同。 - 表实一致:三张配置表字段与 `data/schemas/enemy_table.gd`/`hero_table.gd`/`skill_table.gd` 逐字段一致。 diff --git a/design/gdd/ux/level-map.md b/design/gdd/ux/level-map.md index b83e6ed0..a65cde62 100644 --- a/design/gdd/ux/level-map.md +++ b/design/gdd/ux/level-map.md @@ -2,12 +2,13 @@ > **Status**: 定稿版——三项设计提案已拍板完成(2026-09-27:底栏状态展示 / 图上点击选路·候选按钮退役 / 放弃二次确认);**形态已升格独立屏(2026-09-28 用户裁决:独立存在为最终形态)**;**视觉终稿已冻结(2026-09-28 关卡画面样张 v1 机检 11/11 全绿,闸形 A——冻结值见 Layout/组件清单与样张机检报告,迭代 4)** > **Author**: user + ZCode(关卡画面 UX 批) -> **Last Updated**: 2026-10-15(简明技术中文用语规范批:词面修正 6 处) +> **Last Updated**: 2026-10-25(迭代 35 生成图换代:呈现面随动登记) > **Platform Target**: PC (Steam)——Keyboard/Mouse 主,全部操作纯鼠标可完成(同 [battle.md](battle.md)) > **Template**: UX Spec > **覆盖范围**: 独立屏形态(关卡停点全景)· 入口停点 · 岔路停点 · 单链停点 · 放弃冒险入口 > **不覆盖**: 节点覆盖层(NodeOverlay——奖励/恢复兑付反馈,spec:[node-reward.md](node-reward.md))、商店覆盖层(spec:[shop.md](shop.md))、关卡结算屏([level-clear.md](level-clear.md))、战斗屏([battle.md](battle.md)) > **机制权威**: [关卡节点地图](../systems/冒险循环/关卡节点地图.md)(选路、兑付、停点语义);图的来源 = [关卡节点图生成](../systems/冒险循环/关卡节点图生成.md)(参数化长图——只换图的来源与形态,不换本屏的呈现语义;多终点时各终点均带 Boss 标记,标记语义不变、数量随图;图形态随参数变化,布局自适应,形态不进呈现代码);**系统契约**: [HUD框架](../systems/表现与支撑/HUD框架.md)(订阅纪律、模态层级、可访问性基线) +> **生成图随动登记(2026-10-25)**:入口停点候选数=入口个数(1 或多,多入口并列呈现,首跳即选从哪个入口进场);节点数字=图内句柄 id(非节点深度、非数组序);生成图深度域 L3 上探至 0–13,超出画布冻结公式约 10 层的屏预算——等比缩放与滚动视口两案均未设计,归呈现批(见待办清单「超预算图策略升级路径」条),本 spec 冻结值暂不变。 --- @@ -51,7 +52,7 @@ 本屏必须传达的全部信息,按注视优先级分六层: -1. **节点图本体**(第一注视点):节点方块(五类型字面:战/赏/疗/王/商)、通道连线、当前位置、可选去路 +1. **节点图本体**(第一注视点):节点方块(五类型字面:战/赏/疗/王/商)、通道连线、当前位置、可选去路;类型可见性=邻近 N 层内显示类型,范围外只见结构(机制见[关卡节点地图](../systems/冒险循环/关卡节点地图.md)) 2. **候选选路口**:亮金候选通道 + 候选节点亮金描边——「下一步能去哪」的图上双冗余(2026-09-27 拍板:候选按钮退役,图上点击为唯一提交通道) 3. **常驻语境**(顶栏):关卡名(第 N 层·名称)、GP 常驻总量(2026-09-26 二轮拍板去向:GP = 关卡画面常驻总量,[battle.md](battle.md) OQ14 口径经 2026-09-27 GP 归属拍板对齐批维持) 4. **状态展示**(底栏,2026-09-27 拍板):生命(当前/上限)、属性(攻击 / 护甲 AC 有效值)、本关已选加成(统一冒险加算区增量)——选路规划原料常读 @@ -65,7 +66,7 @@ | 区 | 位置与尺寸 | 内容 | |---|---|---| | **顶栏语境** | 居中横排,图区上方,高约 32px | 左:关卡名(`level_row.display_name`,如「第二层 · 幽暗地域」);右:GP 常驻徽章(GP 图标+数字,直角细线数据框——GP 值域超点阵适用档,走 pattern 5 的 When-NOT-to-Use 路线);HP 归底部状态栏(2026-09-27 拍板完成) | -| **节点图区** | 居中 | 整幅节点图自绘:节点方块 + 通道折线 + 节点态着色 + Boss 紫赤描边。画布随图自适应(**冻结公式**:宽 = 70+层深×122+34、高 = 52+槽位×58+26+8,下限 620×240——routebug 83c11a1 根修件);通道折线**冻结 2/3/4px**(未点亮石灰 STONE/候选亮金 GOLD_LIT/悬停加粗);节点方块 52×36 内**节点图标 20px 烘焙 1:1**(♻ kenney 五键:战=弓/赏=桂冠/疗=篝火/王=王冠/商=钱袋,WARM 单色、当前金底转深墨——双色墨制)+ 编号 10px 右上角 | +| **节点图区** | 居中 | 整幅节点图自绘:节点方块 + 通道折线 + 节点态着色 + Boss 紫赤描边。画布随图自适应(**冻结公式**:宽 = 70+层深×122+44.5、高 = 52+槽位×58+25+24,下限 620×256——routebug 83c11a1 根修件);通道折线**冻结 2/3/4px**(未点亮石灰 STONE/候选亮金 GOLD_LIT/悬停加粗);节点方块 52×36 内**节点图标 20px 烘焙 1:1**(♻ kenney 五键:战=弓/赏=桂冠/疗=篝火/王=王冠/商=钱袋,WARM 单色、当前金底转深墨——双色墨制)+ 编号(图内句柄 id)10px 右上角 | | **底部状态栏** | 图区下方横排,高约 48px | 左-中:状态数据框组(直角细线,pattern 5 族)——生命(当前/上限 + 迷你血条)、属性(攻击 / 护甲 AC,有效值 + 加成角标)、本关加成(攻+X · 上限+Y · AC+Z,零加成隐藏);右端:放弃冒险按钮(危险红族文字,与状态数据框保持间距——破坏性确认异位,pattern 7) | ### Component Inventory @@ -74,7 +75,7 @@ |---|---|---|---|---| | 关卡名标签 | 文本 | `level_row.display_name`(关卡表行读数) | 只读 | 新建(现行标题位升级) | | GP 常驻徽章 | 直角细线数据框 | GP 图标 + 数字(`runner.gp`,冒险钱包读数;刷新经 `wallet_changed` 信号投影〔gain/buy/clear 三事件〕或写路径回执后重查) | 只读 | 新建(GP 去向拍板落地面) | -| 节点图画布 | 自绘 Control | 节点图标(五键 20px 烘焙,双色墨制)+ 编号、通道折线、节点态着色、Boss 紫赤描边+深墨环(BOSS_PURPLE——2026-09-26 封印紫批语义:紫赤本色) | 悬停预承诺 / 点击提交选路(唯一通道) | ✅ level_map_screen.gd `_draw_graph`(视觉终稿已落码——迭代 4 T2/T3) | +| 节点图画布 | 自绘 Control | 节点图标(五键 20px 烘焙,双色墨制)+ 编号(图内句柄 id)、通道折线、节点态着色、Boss 紫赤描边+深墨环(BOSS_PURPLE——2026-09-26 封印紫批语义:紫赤本色) | 悬停预承诺 / 点击提交选路(唯一通道) | ✅ level_map_screen.gd `_draw_graph`(视觉终稿已落码——迭代 4 T2/T3) | | 状态数据框组 | 直角细线数据框 ×3 | 生命(当前/上限 + 迷你血条)、属性(攻击 / 护甲 AC 有效值 + 加成角标)、本关加成(加算区增量列表,零加成隐藏) | 只读 | 新建(2026-09-27 拍板:底栏状态展示) | | 放弃冒险按钮 | 按钮(危险红族) | 「放弃冒险(同失败结算——已获大奖励保留)」;二次确认态已拍板(见布局决策 3) | 点击(首击转确认态、二击提交) | ✅ level_map_screen.gd(可见性归组合根,仅冒险模式开) | | 标题行 | 文本 | 停点语境提示(见布局决策 1 三停点标题) | 只读 | ✅ level_map_screen.gd `_title`(措辞已按布局决策 1 改稿) | @@ -83,7 +84,7 @@ | 停点 | 候选数 | 标题 | |---|---|---| -| 入口停点 | 1(入口节点) | 「出发——选择首个节点」 | +| 入口停点 | 入口个数(1 或多) | 「出发——选择首个节点」 | | 单链停点 | 1 | 「继续前进」 | | 岔路停点 | ≥2 | 「选择下一节点」 | diff --git a/design/gdd/术语表.md b/design/gdd/术语表.md index b4ea59a7..f6d4a876 100644 --- a/design/gdd/术语表.md +++ b/design/gdd/术语表.md @@ -18,6 +18,12 @@ | 装板(装板期) | 把地图战斗资源摇定的类型身份铺设到棋盘格子上的布置动作(阶段);由战斗结算的布置段执行,随机取数标签为 `variant_place` | | 摇号 / 摇定 | 按池配置用随机数决定结果(资源身份、遭遇抽取、货架抽签);随机服务实现见 `game/core/rng_service.gd`(流标签为代码事实) | +## 关卡与地图 + +| 术语 | 定义 | +|---|---| +| 信息迷雾 | 关卡节点图上按邻近层数隐藏节点类型的可见性机制——只隐藏类型,结构(节点、连线、入口、终点位置)恒可见;与「视野迷雾」(战斗棋盘按英雄曼哈顿半径遮挡格子)同词根易混,代码标识符一律不用 `fog_` 前缀。参数 N=信息可见层数(`level.tres` 的 `info_visible_layers`),机制见[关卡节点地图](systems/冒险循环/关卡节点地图.md) | + ## 系统资源与经济 | 术语 | 定义 | diff --git a/game/core/adventure_runner.gd b/game/core/adventure_runner.gd index 379f423a..4823b21e 100644 --- a/game/core/adventure_runner.gd +++ b/game/core/adventure_runner.gd @@ -42,7 +42,7 @@ var _pending_next_level := false # 大奖励已兑付、等 continue_to var _last_level_payload := {} # 最近一次关卡结束载荷(失败结算的败退节点明细来源——数值验证取数面) var _override := [0, 0] # 调试浮窗百分比通道 [atk_pct, hp_pct](每关新 runner 都要应用) var _base_override := {"atk": 0, "max_hp": 0, "armor_class": 0} -var _first_script := false # 首次脚本模式(关卡节点图生成 v0.4——true = 全关零抽签取池内模板 0;测量夹具恒 false) # 永久强化 flat 注入(冒险开始参数面 params.hero_override 存入,共享给 LevelRunner;全零 = 白板) +# 永久强化 flat 注入(冒险开始参数面 params.hero_override 存入,共享给 LevelRunner;全零 = 白板) var _last_treasure_cleared := 0 # additive(最近一次关卡末清空计数,summary 取数面) var _treasure_unlocked := {} # aux 解锁门控集(params.treasure_unlocked 接收面) var _attunement: Array[Variant] = [] # 同调装配集(params.attunement 接收面,start_level 外下传 LevelRunner) @@ -99,7 +99,6 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool: _base_override = _normalize_override(params.get("hero_override", {})) hero_attack_type = String(params.get("attack_type", "")) # 两根类型轴快照(契约 8 锁死:开局后不随存档选中变化) hero_armor_type = String(params.get("armor_type", "")) - _first_script = bool(params.get("first_script", false)) # 缺省 = 非首次(抽签路径;真实侧由呈现层读存档布尔位传入) _shop_mode = "pool" if String(params.get("shop_mode", "legacy")) == "pool" else "legacy" # 缺省 legacy(off 对照零扰动) _treasure_unlocked = params.get("treasure_unlocked", {}) if params.get("treasure_unlocked", {}) is Dictionary else {} _attunement = params.get("attunement", []) if params.get("attunement", []) is Array else [] # additive @@ -144,6 +143,11 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool: if bp.is_empty(): push_error("[冒险结构] resume_adventure 拒绝——空断点快照") return false + if int(bp.get("graph_gen_v2", 0)) != 1: + # 旧代差断点(模板池时代的 node_id 与生成图句柄空间可碰撞错位)——显式拒绝, + # 禁按新语义静默回放;旧模板图已随模板池退役,无迁移动作可做(凭空捏图=兜底图红线)。 + push_error("[冒险结构] resume_adventure 拒绝——断点缺生成图代差标记(graph_gen_v2),属换代前旧断点") + return false master_seed = int(bp.get("master_seed", 0)) rng.restore_adventure(master_seed, int(bp.get("rng_counter", 0))) ledger = {"hero_hp": int(bp.get("hero_hp", 0)), "gp": int(bp.get("gp", 0))} @@ -191,7 +195,6 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool: # 钳制用 flat 已含的保守口径即可,加算区恢复后其余战斗照常)。 ledger["hero_hp"] = mini(int(ledger.get("hero_hp", 0)), maxi(1, int(round(float(int(db.hero.max_hp) + int(_base_override.get("max_hp", 0))) * (100.0 + float(_override[1])) / 100.0)))) - _first_script = false # 续玩模板已随快照强制(已抽模板零重抽)——脚本模式不重放 _shop_mode = "pool" if String(params.get("shop_mode", "legacy")) == "pool" else "legacy" # 续玩参数面同 start_adventure(缺省 legacy) _treasure_unlocked = params.get("treasure_unlocked", {}) if params.get("treasure_unlocked", {}) is Dictionary else {} _attunement = params.get("attunement", []) if params.get("attunement", []) is Array else [] # additive @@ -844,14 +847,12 @@ func _start_current_level() -> bool: _runner.wallet_changed.connect(_forward_wallet_changed) _runner.level_finished.connect(_on_level_finished) _runner.set_hero_override(int(_override[0]), int(_override[1])) - # 断点续玩分支:模板随快照强制(已抽模板零重抽零 RNG)、resume_state 带 - # 停点节点与已到序——重建到停点后经 present_resumed_choice 呈现(信号顺序契约同入口)。 + # 断点续玩分支:图由主种子+参数档重生成同图(产图与计数器号位无关,停点零重抽语义随之消解), + # resume_state 带停点句柄与已到序——重建到停点后经 present_resumed_choice 呈现(信号顺序契约同入口)。 var resume_state: Dictionary[Variant, Variant] = {} - var template_pick: int = 0 if _first_script else -1 # 首次脚本 = 每关固定模板 0 零抽签;否则逐关抽签 if not _resume_bp.is_empty(): resume_state = {"node_id": int(_resume_bp.get("node_id", -1)), "visited_ids": _resume_bp.get("visited_ids", []) as Array} - template_pick = int(_resume_bp.get("template_pick", -1)) # 接口①开始关卡:生命经共享账本交接(账本当前值即开局生命——冒险开始置满 / 大奖励回满已写好); # _base_override 共享引用随载荷传入(永久强化 flat——LevelRunner.effective_* 叠加序第二项)。 # defer_entry=true 起每关(含首关)——先 emit level_started(呈现层在 @@ -860,7 +861,7 @@ func _start_current_level() -> bool: _runner.set_run_ledger(run_ledger) # 共享四账单类下传(run_ledger=四账类,ledger=关卡账) _runner.sync_attune_inventory() # 能力件骑库存面注入(随账下传同拍) var ok: bool = _runner.start_level_external(level_index, ledger, purchased, - apply_rest, _base_override, true, template_pick, resume_state) + apply_rest, _base_override, true, resume_state) if ok: _runner.set_attunement(_attunement) # 同调装配下传(enc 携带进战斗结算) _runner.set_feats(_feats) # 特性持有集下传(begin_battle enc.feats 求值面) @@ -1010,7 +1011,7 @@ func _snapshot_breakpoint() -> void: "shop_shelf": _runner.shop_bp_shelf() if _runner != null else [], # 货架入断点(离开店/非店停点 = 空架合法态) "shop_visit_buys": _runner.shop_bp_visit_buys() if _runner != null else 0, # 本次进店已购次数(A 链定价输入) "attunement_charges": run_ledger.attune_charges.duplicate(true), # 同调主动件充能余量随断点(save v4 域) - "template_pick": _runner.active_template_index if _runner != null else -1, + "graph_gen_v2": 1, # 生成图换代代差标记(缺键 = 换代前旧断点 → resume 显式拒绝) "master_seed": master_seed, "rng_counter": rng.counter(), "keystone": keystone, diff --git a/game/core/battle_orchestrator.gd b/game/core/battle_orchestrator.gd index 1515056b..3cdf75d9 100644 --- a/game/core/battle_orchestrator.gd +++ b/game/core/battle_orchestrator.gd @@ -794,7 +794,7 @@ func _grant_routed(snap: Dictionary, polarity: String, source_class: String, cel if halve_pct > 0 and polarity == "negative" and source_class == "mine_hit": row = row.duplicate() var half: int = int(ceil(float(int(row.power)) * float(100 - halve_pct) / 100.0)) - _emit_row("effect", source_class, cell, ri, "惊巢反制——挖雷惩罚减半(%d→%d)" % [int(row.power), half], 0, {"effect_id": "nest_counter"}, highlight) + _emit_row("effect", source_class, cell, ri, "惊巢反制——危险格挖掘惩罚减半(%d→%d)" % [int(row.power), half], 0, {"effect_id": "nest_counter"}, highlight) row.power = half return _dispatch_row(row, source_class, cell, ri, highlight, label_suffix) diff --git a/game/core/config_db_cross_validator.gd b/game/core/config_db_cross_validator.gd index 7bb4b95f..4e7d8912 100644 --- a/game/core/config_db_cross_validator.gd +++ b/game/core/config_db_cross_validator.gd @@ -51,60 +51,8 @@ func cross_validate(loaded: Dictionary, push_diag: Callable) -> void: for s in required_sources: if not st_tab.source_label_map.has(s): push_diag.call("error", "segorder_violation", "source_label_map 缺必需键: %s" % s) - # ④ LevelTable 节点悬空引用(battle/boss 节点:board_gen 侧须有精确行;enemy 侧按 - # (关卡序号, 行类) 带寻址——落带即设计语义,节点深度高于顶带键=顶 clamp 消解属调参事故,加载期直接拦。 - # 同关同一张图多终点 Boss 节点解析带键须互异(共带=两终点命中同一 Boss 行集,「走哪条路打谁」落空)) - var lv: LevelTable = loaded.get("level") - var bg: BoardGenTable = loaded.get("board_gen") - var en: EnemyTable = loaded.get("enemy") - if lv != null and bg != null and en != null: - var bg_keys := {} - for row in bg.rows: - bg_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true - var en_bands := {} # "%d_%s" % [L, row_kind] → 升序带键数组 - for row in en.rows: - var ek := "%d_%s" % [int(row.level_index), String(row.get("row_kind", ""))] - if not en_bands.has(ek): - en_bands[ek] = [] - var eb := int(row.get("depth_band", -1)) - if not (en_bands[ek] as Array).has(eb): - (en_bands[ek] as Array).append(eb) - for ek in en_bands.keys(): - (en_bands[ek] as Array).sort() - for lrow in lv.rows: - var li: int = int(lrow.level_index) - # 模板池感知(关卡节点图生成 v0.4——池内全模板跨表校验,不只 row.nodes) - var node_lists: Array[Array] = [] - var tpls: Array[Variant] = lrow.get("templates", []) - if tpls.is_empty(): - node_lists.append(lrow.get("nodes", [])) - else: - for tpl in tpls: - node_lists.append(tpl.get("nodes", [])) - for tpl_i in range(node_lists.size()): - var boss_bands := {} # 本图已占 Boss 带键 - for node in node_lists[tpl_i]: - var t: String = String(node.get("type", "")) - if t != "battle" and t != "boss": - continue - var nd: int = int(node.get("node_depth", -1)) - var key := "%d_%d" % [li, nd] - if not bg_keys.has(key): - push_diag.call("error", "level_node_dangling", "level %d %s 节点 (%s) 在 board_gen 无精确行" % [li, t, key]) - var nk := "%d_%s" % [li, t] - if not en_bands.has(nk): - push_diag.call("error", "level_node_dangling", "level %d %s 节点 (%d) 在 enemy 无该行类行集" % [li, t, nd]) - continue - var hit_band: int = EnemyTable.band_of(en_bands[nk], nd) - if hit_band < 0: - push_diag.call("error", "level_node_dangling", "level %d %s 节点深度 %d 高于 (level %d, %s) 顶带键 %d——顶 clamp 消解" % [ - li, t, nd, li, t, int((en_bands[nk] as Array)[-1])]) - continue - if t == "boss": - if boss_bands.has(hit_band): - push_diag.call("error", "rowset_rule", "level %d 模板 %d 多终点 Boss 节点同带(band %d)——共带使「走哪条路打谁」落空" % [li, tpl_i, hit_band]) - boss_bands[hit_band] = true # ⑤ 技能池引用完整性(敌人技能池批——敌人表池 × 技能表;独立入口 validate_skill_pool_links 供冒烟直验) + var en: EnemyTable = loaded.get("enemy") var skt = loaded.get("skill") if skt != null and en != null: for e in validate_skill_pool_links(skt, en, loaded.get("status_effects")): diff --git a/game/core/level_runner.gd b/game/core/level_runner.gd index 85b4c4ea..300534b3 100644 --- a/game/core/level_runner.gd +++ b/game/core/level_runner.gd @@ -64,7 +64,7 @@ var _phase: int = Phase.IDLE var level_result := "" # "" / "victory" / "defeat"(外部契约面) var _candidates: Array[Dictionary] = [] # 当前选路候选(节点引用数组) var visited: Array[Dictionary] = [] # 已兑付/到达节点序列(呈现层图条数据源;含当前节点) -var _graph_mode := false # 图形态缓存(行集任一节点带 next;单链 = false) +var _generator # 参数化长图生成器(惰性建——NodeMapGenerator 持注入的 RngService) func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void: @@ -127,7 +127,7 @@ func restore_hero_base() -> void: # ---- 关卡生命周期 ---- -func start_level(level_index: int, seed_val: int, base_override_in: Dictionary = {}, template_pick: int = -1) -> bool: +func start_level(level_index: int, seed_val: int, base_override_in: Dictionary = {}) -> bool: # 单关测试模式:私有账本、内置恢复数学、自调 new_adventure(漂移① # 单关=冒险的历史简化在测试模式保留;正式种子域归 AdventureRunner.start_adventure) # base_override_in = 永久强化 flat 注入尾参(缺省 {} 全零白板, @@ -146,34 +146,31 @@ func start_level(level_index: int, seed_val: int, base_override_in: Dictionary = _phase = Phase.IDLE _candidates = [] visited = [] - _graph_mode = false gp = 0 # 冒险钱包清零开局(局内货币生命周期起点) run_ledger.bonus = {"atk": 0, "max_hp": 0, "armor_class": 0} # 加算区冒险开始全 0(单关入口零元进度——注入面见上) base_override = {"atk": 0, "max_hp": 0, "armor_class": 0} # 永久强化 flat:缺省全零白板(单关入口零元进度——注入面见上) _shop_purchased = {} # 已购档数清零(跨冒险不延续——递增定价按冒险内累计) for k in base_override.keys(): base_override[k] = maxi(0, int(base_override_in.get(k, 0))) # 注入规范化(契约同冒险开始参数面) - for node in nodes(): - if node.has("next"): - _graph_mode = true - break hero_hp = effective_max_hp() rng.new_adventure(seed_val) # 关卡起点 = 冒险起点(切片口径;跨关接续归 冒险结构) - level_row.assign(_resolve_template_row(row, template_pick)) # 池解析在播种后(node_map 抽签须已播种——EC7) - _phase = Phase.RUNNING - var entry := _entry_node() - if entry.is_empty(): - push_error("[关卡节点地图] 入口节点缺失(无入边唯一节点——图合法性校验应已拦截)") + if not _assign_generated_graph(row, seed_val): _phase = Phase.ENDED return false - return _enter_node(entry) + _phase = Phase.RUNNING + var entries := _entry_nodes() + if entries.is_empty(): + push_error("[关卡节点地图] 入口节点缺失(无入边节点集为空——validate_graph 应已拦)") + _phase = Phase.ENDED + return false + return _enter_node(entries[0]) # 冒险结构接口①:开始关卡(载荷 = 关卡序号 + 当前生命 + 生效上限——生命经共享账本交接; # 种子域归冒险结构:本路径不重播种;账本归冒险结构:本路径不重置 gp/bonus/_shop_purchased, # 也不重置 hero_hp——账本当前值即开局生命,AdventureRunner 已在 ledger 写好)。 # base_override = 永久强化 flat 共享引用(缺省 {} 全零白板,与 B1 空参数路径同值)。 -# 流程与 start_level 同款(_find_level_row → 状态初始化 → _graph_mode 探测 → _enter_node(entry)), +# 流程与 start_level 同款(_find_level_row → 生成图装配 → 状态初始化 → _enter_node(入口)), # 差异仅在账本/加算区/已购档/恢复执行体的注入与零重置。 # defer_entry 尾参(缺省 false = 直接进节点)——true 时不自动 # _enter_node(entry),改置选路态(awaiting_choice + 单候选 = 入口节点),由 AdventureRunner 在 @@ -183,7 +180,7 @@ func start_level(level_index: int, seed_val: int, base_override_in: Dictionary = # _enter_node(entry)——测量面零漂移红线。 func start_level_external(level_index: int, ledger: Dictionary, purchased_ref: Dictionary, rest_handler: Callable, base_override_ref: Dictionary = {}, - defer_entry: bool = false, template_pick: int = -1, resume_state: Dictionary = {}) -> bool: + defer_entry: bool = false, resume_state: Dictionary = {}) -> bool: var row := _find_level_row(level_index) if row.is_empty(): push_error("[关卡节点地图] level_index %d 在 LevelTable 无行" % level_index) @@ -193,34 +190,30 @@ func start_level_external(level_index: int, ledger: Dictionary, base_override = base_override_ref _shop_purchased = purchased_ref _rest_handler = rest_handler - level_row.assign(_resolve_template_row(row, template_pick)) + if not _assign_generated_graph(row, rng.master_seed): + return false node_index = -1 level_result = "" _phase = Phase.IDLE _candidates = [] visited = [] - _graph_mode = false - for node in nodes(): - if node.has("next"): - _graph_mode = true - break _phase = Phase.RUNNING # 断点续玩路径(defer_entry 之外的第三种停点入口):直接重建到停点所在节点, - # 零 RNG 消耗(模板已随 template_pick 强制、节点状态纯重建不 _enter_node——当场战斗从头打)。 + # 零 RNG 消耗(图由主种子+参数档重生成同图、节点状态纯重建不 _enter_node——当场战斗从头打)。 # resume_state = {"node_id": int(-1 = 入口停点), "visited_ids": Array[int]};空字典 = 既有路径逐位不变。 if not resume_state.is_empty(): return _restore_to_stop(int(resume_state.get("node_id", -1)), resume_state.get("visited_ids", []) as Array) - var entry := _entry_node() - if entry.is_empty(): - push_error("[关卡节点地图] 入口节点缺失(无入边唯一节点——图合法性校验应已拦截)") + var entries := _entry_nodes() + if entries.is_empty(): + push_error("[关卡节点地图] 入口节点缺失(无入边节点集为空——validate_graph 应已拦)") _phase = Phase.ENDED return false if defer_entry: - _phase = Phase.AWAITING_CHOICE # 入口停点(node_index 保持 -1——进节点前无 current_node) - _candidates = [entry] + _phase = Phase.AWAITING_CHOICE # 入口停点(node_index 保持 -1)——候选=全部入口(多入口首跳即选路) + _candidates = entries return true - return _enter_node(entry) + return _enter_node(entries[0]) # 入口停点呈现(defer_entry=true 的第二步):发出入口候选供呈现层弹 @@ -240,14 +233,14 @@ func present_entry_choice() -> bool: # 候选 = 停点节点出边(与 choices_presented 停点语义逐位一致)。 func _restore_to_stop(node_id: int, visited_ids: Array) -> bool: if node_id < 0: - # 入口停点:与 defer_entry 同款形态(node_index 保持 -1,单候选 = 入口节点) - var entry := _entry_node() - if entry.is_empty(): + # 入口停点:与 defer_entry 同款形态(node_index 保持 -1,候选 = 全部入口) + var entries := _entry_nodes() + if entries.is_empty(): push_error("[关卡节点地图] 断点恢复失败——入口节点缺失") _phase = Phase.ENDED return false _phase = Phase.AWAITING_CHOICE - _candidates = [entry] + _candidates = entries return true var stop: Dictionary[Variant, Variant] = {} for node in nodes(): @@ -296,32 +289,31 @@ func _find_level_row(level_index: int) -> Dictionary: return db.level_row(level_index) # 表访问收口(行集直扫归 LevelTable/ConfigDB 访问器) -# ---- 模板池解析(关卡节点图生成 v0.4——M-3)---- -# row.templates 非空 = 池形态:template_pick >= 0 = 强制取该模板(首次脚本模式/冒烟固定图,零 RNG 消费); -# -1 = 抽签(node_map 用途标签恰一 scope,均匀取模)。空池 = 旧单图形态原样返回(向后兼容零迁移)。 -# 解析产物 = row 副本换 nodes(下游 nodes()/校验/呈现全透明);active_template_name 供 MC 分层出数; -# active_template_index 供断点快照备案(已抽模板续玩零重抽零 RNG 消耗)。 -var active_template_name := "" -var active_template_index := -1 # -1 = 非池形态(旧单图)/未解析 - -func _resolve_template_row(row: Dictionary, template_pick: int) -> Dictionary: - var templates: Array[Variant] = row.get("templates", []) - active_template_name = "" - active_template_index = -1 - if templates.is_empty(): - return row - var idx := 0 - if template_pick >= 0: - idx = clampi(template_pick, 0, templates.size() - 1) - else: - var stream: RandomNumberGenerator = rng.open_stream("node_map") - idx = stream.randi_range(0, templates.size() - 1) - var tpl: Dictionary[Variant, Variant] = templates[idx] - active_template_name = String(tpl.get("display_name", "")) - active_template_index = idx - var out := row.duplicate(true) - out["nodes"] = tpl.get("nodes", []) - return out +# ---- 图源装配(关卡节点图生成 v1.1——图 = f(参数档, 种子) 纯函数;无模板池、无兜底图)---- +# 参数档按关卡序号解析(消费侧职责——生成器不认关卡号),种子取本局口径:单关模式用传入种子、 +# 冒险模式用主种子(产图与全局计数器号位无关,断点在停点重生成同图)。产图先过消费侧独立校验 +# LevelTable.validate_graph(与产出侧自证同源规格、分工不重叠);违规=起关拒绝,不降级不回退。 +func _assign_generated_graph(row: Dictionary, seed_val: int) -> bool: + var level_index: int = int(row.get("level_index", 0)) + var params: Dictionary[Variant, Variant] = db.level_gen_params_row(level_index) + if params.is_empty(): + push_error("[关卡节点地图] level %d 无参数档行(level_gen_params 缺行——配置错误)" % level_index) + return false + if _generator == null: + _generator = NodeMapGenerator.new(rng) + var graph: Dictionary[Variant, Variant] = _generator.generate(params, seed_val) + if graph.is_empty() or _generator.is_fatal(): + push_error("[关卡节点地图] level %d 生成图失败(参数无解或产出不合格——无兜底图)" % level_index) + return false + var bad := LevelTable.validate_graph(graph["nodes"] as Array) + if not bad.is_empty(): + for e in bad: + push_error("[关卡节点地图] level %d 生成图违规: %s" % [level_index, String(e["message"])]) + return false + var merged := row.duplicate(true) + merged["nodes"] = graph["nodes"] + level_row.assign(merged) + return true func nodes() -> Array: @@ -334,37 +326,30 @@ func current_node() -> Dictionary: return nodes()[node_index] -# ---- 图版行进面(v1.0:出边表选路 · 零自动推进 · 单链 = 隐式出边退化形态)---- -# 编号空间(与 LevelTable 校验器同一解析):图形态 = node_depth(全节点编号化);单链 = 数组下标。 -# 混用两空间会在单链上碰撞(如 reward 下标 2 vs 战斗 node_depth 2)——节点寻址一律经 _node_id。 +# ---- 图版行进面(生成图恒带显式出边;单链退化形态 = 出度 ≤1 的图,无数组序隐式边)---- +# 节点身份=生成图句柄 id(图内唯一)。定位信息另算(enc 携带关卡序号 + 节点深度), +# 禁再以深度当身份(句柄/位置两轴语义单一)。 func _node_id(node: Dictionary) -> int: - if _graph_mode: - return int(node.get("node_depth", -1)) - return nodes().find(node) + return int(node.get("id", -1)) -# 入口节点 = 无入边唯一节点(图合法性校验保证恰一) -func _entry_node() -> Dictionary: +# 入口节点集 = 全部无入边节点(个数=参数档 entry_count;多入口时首跳即选路) +func _entry_nodes() -> Array[Dictionary]: var inbound := {} for node in nodes(): for tid in _out_ids(node): - inbound[tid] = true + inbound[int(tid)] = true + var out: Array[Dictionary] = [] for node in nodes(): if not inbound.has(_node_id(node)): - return node - return {} + out.append(node) + return out -# 出边编号表:显式 next 优先;单链(无 next 键)= 数组顺序下一项(退化形态) +# 出边句柄表(生成图恒显式 next) func _out_ids(node: Dictionary) -> Array: - if node.has("next"): - return node.get("next", []) - var nodes: Array[Variant] = nodes() - var i := nodes.find(node) - if i >= 0 and i < nodes.size() - 1: - return [i + 1] - return [] + return node.get("next", []) # 出边候选(节点引用数组——选路口/呈现层共用读面) @@ -442,11 +427,13 @@ func type_visible_ids() -> Array[int]: # 不可达节点不落表(加载期孤儿校验应已拦截)。 func _bfs_layers() -> Dictionary: var layers := {} - var entry := _entry_node() - if entry.is_empty(): + var entries := _entry_nodes() + if entries.is_empty(): return layers - var queue: Array[Dictionary] = [entry] - layers[_node_id(entry)] = 0 + var queue: Array[Dictionary] = [] + for entry in entries: + layers[_node_id(entry)] = 0 + queue.append(entry) var i := 0 while i < queue.size(): var cur: Dictionary[Variant, Variant] = queue[i] @@ -510,7 +497,7 @@ func _start_battle(node: Dictionary) -> bool: # node_entered 先于 begin_battle:呈现层须先拿到 settlement 引用,才能响应 begin 期间发出的 # board_changed(首板安装)并重建棋盘——后发会导致首战棋盘不渲染 node_entered.emit(node, {}) - if not _settlement.begin_battle({"level_index": int(level_row.level_index), "node_depth": int(node.get("node_depth", 0)), + if not _settlement.begin_battle({"level_index": int(level_row.level_index), "node_depth": int(node.get("depth", 0)), "hero_hp": hero_hp, "hero_atk": effective_atk(), "hero_max_hp": effective_max_hp(), "hero_armor_class": effective_ac(), "hero_ap_bonus": effective_ap_bonus(), "attack_type": _hero_attack_type, "armor_type": _hero_armor_type, # 两根类型轴(spawn 英雄快照;空串=锻造配置默认值回退) @@ -523,7 +510,7 @@ func _start_battle(node: Dictionary) -> bool: "voucher_rap": _voucher_rap, "voucher_time_sand": _voucher_time_sand, "node_type": String(node.get("type", ""))}): - push_error("[关卡节点地图] 战斗启动失败(depth %s)" % str(node.get("node_depth", -1))) + push_error("[关卡节点地图] 战斗启动失败(depth %s)" % str(node.get("depth", -1))) return false _voucher_rap = 0 # 节点级一次性:注入即消费(战斗未启动则保留至下次注入前不清) _voucher_time_sand = 0 @@ -535,7 +522,7 @@ func _forward_step_row(row: Dictionary) -> void: # battle_index=第几场战斗(关卡域 1 基序——visited 已含当前节点〔_enter_node :437 先追加后启动〕; -# 按已到节点 type=battle 计数,与图形态 node_depth/单链数组下标两编号空间解耦〔混编碰撞免疫〕; +# 按已到节点 type=battle 计数,与句柄 id / 节点深度两轴解耦〔身份与位置互不兼任〕; # 开局姿态组「每关首轮窗」条件面) func _battle_index() -> int: var n := 0 diff --git a/game/core/save_manager.gd b/game/core/save_manager.gd index 9c09daf3..e5475a88 100644 --- a/game/core/save_manager.gd +++ b/game/core/save_manager.gd @@ -299,9 +299,10 @@ func _default_data() -> Dictionary[String, Variant]: } -# ---- 首次冒险布尔位(关卡节点图生成 v0.4)---- -# false = 首次脚本模式(全关零抽签取池内模板 0=现行固定图);置位只在冒险收束挂点(MetaProgression.settle_adventure)。 -# 老档迁移:缺字段 = 该档已有游玩史 → true(不重放教学脚本)。 +# ---- 首次冒险布尔位(关卡节点图生成 v1.1 确定性承载)---- +# false = 本档尚无游玩史 → 首局走固定种子 42 + 参数档生成图(教学与存档复现的固定图由此承载,无兜底模板); +# 置位只在冒险收束挂点(MetaProgression.settle_adventure)。 +# 老档迁移:缺字段 = 该档已有游玩史 → true(不重放首局固定图)。 func is_first_adventure_done() -> bool: return bool(_data["meta"].get("first_adventure_done", true)) diff --git a/game/data/schemas/level_table.gd b/game/data/schemas/level_table.gd index d37a4061..4d66fbc1 100644 --- a/game/data/schemas/level_table.gd +++ b/game/data/schemas/level_table.gd @@ -1,13 +1,14 @@ ## VERTICAL SLICE - NOT FOR PRODUCTION -## 关卡节点表 schema:逐关节点链(单链/节点图两形态)的类型化读取(垂直切片验证:线性第一关承载元进度循环)。 -## 两形态同跑一套图合法性算法,违例加载期致命(validate 报错由 ConfigDB 收敛为 fail-fast)。 +## 关卡节点表 schema:逐关行集(关数/显示名/休整回复/迷雾层数)的类型化读取(垂直切片验证:改表即改关卡)。 +## 图合法性只在 `validate_graph` 一处陈述(消费侧起关校验入口——关卡节点图生成 v1.1 产出规格为准); +## 表加载期不校验图(关卡表不再携带节点数据,图源=参数化生成器)。 class_name LevelTable extends Resource @export var rows: Array[Dictionary] = [] # Array[Dictionary] @export var info_visible_layers: int = 2 # 信息迷雾可见层数 N(int ≥1;类型仅在邻近 N 层内可见——关卡节点地图 v2.1) -const NODE_TYPES := ["battle", "reward", "rest", "boss", "shop"] # 五类(关卡节点地图 v1.1) +const NODE_TYPES := ["battle", "reward", "rest", "boss", "shop"] # 五类(关卡节点地图 v2.1) # 行单点读取(表访问收口——LevelRunner._find_level_row 禁直扫;键 = level_index) @@ -38,182 +39,133 @@ func validate() -> Array: var heal: int = int(row.get("rest_heal_pct", -1)) if heal < 0 or heal > 100: errs.append({"category": "domain", "message": "level %d rest_heal_pct 越域 [0,100]" % L}) - # 模板池(关卡节点图生成 v0.4):row.templates 非空 = 池形态,池内全模板加载期校验 - # (违规致命——不只抽中的那张);空 = 单图形态(row.nodes 直读,向后兼容)。 - var node_lists: Array[Array] = [] # [ [上下文标签, nodes 数组], ... ] - var templates: Array[Variant] = row.get("templates", []) - if templates.is_empty(): - node_lists.append(["", row.get("nodes", [])]) - else: - for j in templates.size(): - var tpl = templates[j] - if typeof(tpl) != TYPE_DICTIONARY or (tpl.get("nodes", []) as Array).is_empty(): - errs.append({"category": "domain", "message": "level %d 模板[%d] 缺 nodes 或为空" % [L, j]}) - continue - node_lists.append(["模板[%d]=%s" % [j, String(tpl.get("display_name", ""))], tpl.get("nodes", [])]) - for nl in node_lists: - for e in _validate_level_nodes(L, nl[0], nl[1]): - errs.append(e) return errs -# 单节点链校验体(validate 抽出——row.nodes 与池内每张模板同跑一套;ctx = 消息上下文标签) -func _validate_level_nodes(L: int, ctx: String, nodes: Array) -> Array: - var errs: Array[Dictionary] = [] - var tag: String = "" if ctx.is_empty() else "〔%s〕" % ctx +# 图合法性校验单点(消费侧起关入口——生成图到手、进节点前调用;违规=致命,无软回退)。 +# 判据=四检+良构基底:① 入口(无入边)≥1 且终点(无出边)≥1;② 全部节点从某入口可达且可达某终点 +#(每条路径恰从一个入口出发、恰到好处一个终点,无孤儿无断头);③ 终点恒为 Boss(终点 type=boss 且 boss 无出边); +# ④ 无环(出边目标深度严格大于源)。良构=句柄唯一·出边全命中·type 在五类枚举·battle/boss 带深度。 +# 入口/终点个数与参数档的一致性归产出侧(关卡节点图生成 v1.1),本处不核个数;商店位归冒烟断言把关,本处禁检。 +static func validate_graph(nodes: Array) -> Array: if nodes.is_empty(): - errs.append({"category": "domain", "message": "level %d%s 节点链为空" % [L, tag]}) - return errs - var boss_count := 0 - var battle_count := 0 - var is_graph := false - for node in nodes: - if node.has("next"): - is_graph = true - var t: String = String(node.get("type", "")) - if not NODE_TYPES.has(t): - errs.append({"category": "enum", "message": "level %d%s 未知节点类型: %s" % [L, tag, t]}) - continue - if t == "battle" or t == "boss": - if int(node.get("node_depth", -1)) < 0: - errs.append({"category": "domain", "message": "level %d%s %s 节点缺 node_depth" % [L, tag, t]}) - battle_count += 1 - if t == "boss": - boss_count += 1 - if boss_count != 1: - errs.append({"category": "structural", "message": "level %d%s Boss 节点数 = %d(须恰一)" % [L, tag, boss_count]}) - if battle_count < 2: - errs.append({"category": "structural", "message": "level %d%s 战斗节点(含 Boss)= %d(≥2 才构成关卡)" % [L, tag, battle_count]}) - if not is_graph: - # 单链形态纪律(第一关零迁移):末节点必须是 Boss(关底收尾——冒险结构 关卡定义) - if String(nodes[nodes.size() - 1].get("type", "")) != "boss": - errs.append({"category": "structural", "message": "level %d%s 末节点非 Boss(单链以 Boss 收尾)" % [L, tag]}) - # 图合法性(单链与图同一套算法——单链 = 出边恰一的退化形态,违规同样加载致命) - for e in _validate_graph(L, nodes, is_graph, tag): - errs.append(e) - # U2 目检规则(两条,加载期同检):①单链前三位纯战斗(奖励/休整禁入前三) - if not is_graph and nodes.size() >= 3: - for i in 3: - if String(nodes[i].get("type", "")) != "battle": - errs.append({"category": "structural", "message": "level %d%s 第 %d 位非战斗(单链前三位纯战斗——U2 目检规则)" % [L, tag, i + 1]}) - return errs + return [{"category": "structural", "message": "图节点集为空"}] + var well := _check_nodes_wellformed(nodes) + if not (well["errs"] as Array).is_empty(): + return well["errs"] + var by_id: Dictionary = well["by_id"] + var edges := _check_edges(by_id, well["indeg"]) + if not (edges["errs"] as Array).is_empty(): + return edges["errs"] + var stops := _check_terminals(by_id, edges["outs"], edges["indeg"]) + if not (stops["errs"] as Array).is_empty(): + return stops["errs"] + return _check_reachability(by_id, edges["outs"], stops["entries"], stops["terminals"]) -# 图合法性核心(返回错误列表):id 解析(图=node_depth / 单链=数组下标)→ 出边表 → -# 唯一性/前进方向/入口唯一/Boss 终点/双向可达六检。参考 关卡节点地图 v1.0 分支节点图规则。 -func _validate_graph(L: int, nodes: Array, is_graph: bool, tag: String = "") -> Array: +# 良构基底:字段齐·type 在枚举·深度非负·句柄唯一(返回 by_id 与入度骨架供后续检复用) +static func _check_nodes_wellformed(nodes: Array) -> Dictionary: var errs: Array[Dictionary] = [] - var ids: Array[int] = [] # 每节点 id(下标对齐) - var nexts: Array[Array] = [] # 每节点出边 id 数组 - var id_to_idx := {} + var by_id := {} + var indeg := {} for i in nodes.size(): var node: Dictionary[Variant, Variant] = nodes[i] - var nid: int = int(node.get("node_depth", i)) if is_graph else i - if is_graph and not node.has("node_depth"): - errs.append({"category": "structural", "message": "level %d%s 图形态节点[%d] 缺 node_depth(全节点须编号化)" % [L, tag, i]}) - nid = i # 降级继续(后续校验仍有意义) - ids.append(nid) - if id_to_idx.has(nid): - errs.append({"category": "dup_key", "message": "level %d%s 节点编号重复: %d(关卡内唯一定位地址)" % [L, tag, nid]}) - id_to_idx[nid] = i - var nx: Array[int] = [] - if node.has("next"): - for v in node.get("next", []): - nx.append(int(v)) - elif i < nodes.size() - 1: - nx.append(i + 1) # 单链隐式出边 = 数组下一项(id 即下标) - nexts.append(nx) - # ② 出边引用存在 + 前进方向(目标 id 严格递增 ⇒ 无环——环必含非递增边) - for i in nodes.size(): - for tid in nexts[i]: - if not id_to_idx.has(tid): - errs.append({"category": "dangling_next", "message": "level %d%s 节点 %d 出边指向不存在编号: %d" % [L, tag, ids[i], tid]}) - elif tid <= ids[i]: - errs.append({"category": "structural", "message": "level %d%s 连线 %d→%d 违反前进方向(目标编号须严格更大)" % [L, tag, ids[i], tid]}) - # ③b 出度上限(「节点出度最大为 2」):单节点出边 >2 即加载致命。 - # 规则连带:出度 ≤2 + 入口唯一下 BFS 层宽上限 = 4(序列 1,2,4——层宽上限 5 与出度约束 - # 不相容,模板池按层宽 ≤4 构造)。 - for i in nodes.size(): - if nexts[i].size() > 2: - errs.append({"category": "fanout_violation", "message": "level %d%s 节点 %d 出度 %d > 2(出度上限拍板 2026-10-01)" % [L, tag, ids[i], nexts[i].size()]}) - # ④ 入口唯一(无入边节点恰一——唯一入口) - var has_inbound := {} - for i in nodes.size(): - for tid in nexts[i]: - has_inbound[tid] = true - var entries: Array[int] = [] - for i in nodes.size(): - if not has_inbound.has(ids[i]): - entries.append(i) - if entries.size() != 1: - errs.append({"category": "structural", "message": "level %d%s 入口节点数 = %d(须恰一——无入边节点唯一)" % [L, tag, entries.size()]}) - # ⑤ Boss 恒唯一终点:无出边 + 编号最大(图形态;单链由"末节点 Boss"既有校验覆盖终点性) - for i in nodes.size(): - if String(nodes[i].get("type", "")) != "boss": + for k in ["id", "type", "depth", "next"]: + if not node.has(k): + errs.append({"category": "structural", "message": "节点[%d] 缺字段 %s" % [i, k]}) + var t := String(node.get("type", "")) + if not NODE_TYPES.has(t): + errs.append({"category": "enum", "message": "未知节点类型: %s" % t}) + var d: int = int(node.get("depth", -1)) + if d < 0: + errs.append({"category": "domain", "message": "节点 %s 深度越域(≥0): %d" % [str(node.get("id", i)), d]}) + if t == "battle" and not node.has("depth"): + errs.append({"category": "structural", "message": "battle 节点 %s 缺深度" % str(node.get("id", i))}) + var nid: int = int(node.get("id", i)) + if by_id.has(nid): + errs.append({"category": "dup_key", "message": "节点句柄重复: %d" % nid}) + by_id[nid] = node + if not indeg.has(nid): + indeg[nid] = 0 + return {"errs": errs, "by_id": by_id, "indeg": indeg} + + +# 出边良构+无环判据(④):目标句柄存在、目标深度严格大于源;同时产出去边表与入度计数 +static func _check_edges(by_id: Dictionary, indeg: Dictionary) -> Dictionary: + var errs: Array[Dictionary] = [] + var outs := {} + for nid in by_id.keys(): + outs[nid] = [] + for nid in by_id.keys(): + for tid in (by_id[nid]["next"] as Array): + if not by_id.has(int(tid)): + errs.append({"category": "dangling_next", "message": "节点 %d 出边指向不存在句柄: %s" % [nid, tid]}) + continue + if int(by_id[int(tid)]["depth"]) <= int(by_id[nid]["depth"]): + errs.append({"category": "structural", "message": "连线 %d→%d 违反前进方向(目标深度须严格大于源)" % [nid, int(tid)]}) + outs[nid].append(int(tid)) + indeg[int(tid)] = int(indeg[int(tid)]) + 1 + return {"errs": errs, "outs": outs, "indeg": indeg} + + +# 检①+检③:入口(无入边)≥1、终点(无出边)≥1、终点恒为 Boss 且 Boss 无出边 +static func _check_terminals(by_id: Dictionary, outs: Dictionary, indeg: Dictionary) -> Dictionary: + var errs: Array[Dictionary] = [] + var entries := [] + var terminals := [] + for nid in by_id.keys(): + if int(indeg[nid]) == 0: + entries.append(nid) + if not (outs[nid] as Array).is_empty(): + if String(by_id[nid]["type"]) == "boss": + errs.append({"category": "structural", "message": "Boss 节点 %d 带出边(终点不可外出)" % nid}) continue - if not nexts[i].is_empty(): - errs.append({"category": "structural", "message": "level %d Boss 节点 %d 带出边(终点不可外出)" % [L, ids[i]]}) - if is_graph and ids[i] != ids.max(): - errs.append({"category": "structural", "message": "level %d Boss 编号 %d 非最大 %d(终点编号最大)" % [L, ids[i], ids.max()]}) - if entries.size() == 1: - # ⑥ 从入口可达全部(无孤儿)· ⑦ 全部节点可达 Boss(逆向)——邻接表统一下标空间 - var fwd := [] - var back := [] - for i in nodes.size(): - fwd.append([]) - back.append([]) - for i in nodes.size(): - for tid in nexts[i]: - if id_to_idx.has(tid): - fwd[i].append(id_to_idx[tid]) - back[id_to_idx[tid]].append(i) - var reach := _reachable(entries[0], fwd, nodes.size()) - for i in nodes.size(): - if not reach.has(i): - errs.append({"category": "structural", "message": "level %d 节点 %d 从入口不可达(孤儿节点)" % [L, ids[i]]}) - var boss_idx := -1 - for i in nodes.size(): - if String(nodes[i].get("type", "")) == "boss": - boss_idx = i - if boss_idx >= 0: - var to_boss := _reachable(boss_idx, back, nodes.size()) - for i in nodes.size(): - if not to_boss.has(i): - errs.append({"category": "structural", "message": "level %d 节点 %d 沿连线不可达 Boss" % [L, ids[i]]}) - # ⑧ U2 目检规则:BFS 同深度层节点数上限 5(分支宽度天花板) - var layer_of := {} - var bfs: Array[Variant] = [entries[0]] - layer_of[entries[0]] = 0 - while not bfs.is_empty(): - var cur: int = bfs.pop_front() - for tid in nexts[cur]: - var tidx: int = int(id_to_idx.get(tid, -1)) - if tidx >= 0 and not layer_of.has(tidx): - layer_of[tidx] = int(layer_of[cur]) + 1 - bfs.append(tidx) - var width := {} - for i in nodes.size(): - if layer_of.has(i): - width[layer_of[i]] = int(width.get(layer_of[i], 0)) + 1 - for d in width.keys(): - if int(width[d]) > 5: - errs.append({"category": "structural", "message": "level %d BFS 第 %d 层 %d 节点(同层上限 5——U2 目检规则)" % [L, d, int(width[d])]}) + terminals.append(nid) + if String(by_id[nid]["type"]) != "boss": + errs.append({"category": "structural", "message": "终点 %d 非 Boss(终点恒为 Boss)" % nid}) + if entries.is_empty(): + errs.append({"category": "structural", "message": "入口节点数 = 0(须 ≥1)"}) + if terminals.is_empty(): + errs.append({"category": "structural", "message": "终点节点数 = 0(须 ≥1)"}) + return {"errs": errs, "entries": entries, "terminals": terminals} + + +# 检②:全部节点从某入口可达且可达某终点(无孤儿、无断头) +static func _check_reachability(by_id: Dictionary, outs: Dictionary, entries: Array, terminals: Array) -> Array: + var errs: Array[Dictionary] = [] + var from_entry := {} + for eid in entries: + _mark_reachable(int(eid), outs, from_entry) + var to_terminal := {} + for tid in terminals: + _mark_backward(int(tid), outs, to_terminal) + for nid in by_id.keys(): + if not from_entry.has(nid): + errs.append({"category": "structural", "message": "节点 %d 从任何入口都不可达(孤儿)" % nid}) + if not to_terminal.has(nid): + errs.append({"category": "structural", "message": "节点 %d 沿连线到不了任何终点(断头)" % nid}) return errs -# BFS 可达集(邻接表 adj: 节点下标 → 下标数组) -func _reachable(start: int, adj: Array, n: int) -> Dictionary: - var seen := {} - if start < 0 or start >= n: - return seen +# 正向可达集(含起点) +static func _mark_reachable(start: int, outs: Dictionary, seen: Dictionary) -> void: var queue: Array[Variant] = [start] seen[start] = true while not queue.is_empty(): var cur: int = queue.pop_front() - if cur >= adj.size(): - continue - for nb in adj[cur]: - if not seen.has(nb): - seen[nb] = true - queue.append(nb) - return seen + for tid in outs[cur]: + if not seen.has(int(tid)): + seen[int(tid)] = true + queue.append(int(tid)) + + +# 逆向可达集(含起点)——反向邻接由 outs 现推(图规模小,禁第二份邻接表持有) +static func _mark_backward(target: int, outs: Dictionary, seen: Dictionary) -> void: + var queue: Array[Variant] = [target] + seen[target] = true + while not queue.is_empty(): + var cur: int = queue.pop_front() + for nid in outs.keys(): + if (outs[nid] as Array).has(cur) and not seen.has(int(nid)): + seen[int(nid)] = true + queue.append(int(nid)) diff --git a/game/data/tables/board_gen.tres b/game/data/tables/board_gen.tres index a783327e..15d9393b 100644 --- a/game/data/tables/board_gen.tres +++ b/game/data/tables/board_gen.tres @@ -70,6 +70,32 @@ rows = [{ "shave_ring_radius": 1, "r0_max_cap": 43 }, { +"level_index": 1, +"node_depth": 5, +"board_width": 12, +"board_height": 12, +"mine_density": 0.25, +"inferability_target": 0.88, +"solver_rules": "single_subset", +"min_safe_count": 60, +"max_gen_attempts": 24, +"board_type": "dungeon", +"shave_ring_radius": 1, +"r0_max_cap": 43 +}, { +"level_index": 1, +"node_depth": 6, +"board_width": 12, +"board_height": 12, +"mine_density": 0.25, +"inferability_target": 0.88, +"solver_rules": "single_subset", +"min_safe_count": 60, +"max_gen_attempts": 24, +"board_type": "dungeon", +"shave_ring_radius": 1, +"r0_max_cap": 43 +}, { "level_index": 2, "node_depth": 0, "board_width": 12, @@ -97,6 +123,32 @@ rows = [{ "r0_max_cap": 43 }, { "level_index": 2, +"node_depth": 2, +"board_width": 12, +"board_height": 12, +"mine_density": 0.25, +"inferability_target": 0.88, +"solver_rules": "single_subset", +"min_safe_count": 60, +"max_gen_attempts": 24, +"board_type": "underdark", +"shave_ring_radius": 1, +"r0_max_cap": 43 +}, { +"level_index": 2, +"node_depth": 3, +"board_width": 12, +"board_height": 12, +"mine_density": 0.25, +"inferability_target": 0.88, +"solver_rules": "single_subset", +"min_safe_count": 60, +"max_gen_attempts": 24, +"board_type": "underdark", +"shave_ring_radius": 1, +"r0_max_cap": 43 +}, { +"level_index": 2, "node_depth": 4, "board_width": 12, "board_height": 12, @@ -149,6 +201,19 @@ rows = [{ "r0_max_cap": 43 }, { "level_index": 2, +"node_depth": 8, +"board_width": 12, +"board_height": 12, +"mine_density": 0.25, +"inferability_target": 0.88, +"solver_rules": "single_subset", +"min_safe_count": 60, +"max_gen_attempts": 24, +"board_type": "underdark", +"shave_ring_radius": 1, +"r0_max_cap": 43 +}, { +"level_index": 2, "node_depth": 9, "board_width": 12, "board_height": 12, @@ -188,6 +253,32 @@ rows = [{ "r0_max_cap": 51 }, { "level_index": 3, +"node_depth": 2, +"board_width": 13, +"board_height": 13, +"mine_density": 0.25, +"inferability_target": 0.88, +"solver_rules": "single_subset", +"min_safe_count": 70, +"max_gen_attempts": 32, +"board_type": "deep_underdark", +"shave_ring_radius": 1, +"r0_max_cap": 51 +}, { +"level_index": 3, +"node_depth": 3, +"board_width": 13, +"board_height": 13, +"mine_density": 0.25, +"inferability_target": 0.88, +"solver_rules": "single_subset", +"min_safe_count": 70, +"max_gen_attempts": 32, +"board_type": "deep_underdark", +"shave_ring_radius": 1, +"r0_max_cap": 51 +}, { +"level_index": 3, "node_depth": 4, "board_width": 13, "board_height": 13, @@ -240,6 +331,19 @@ rows = [{ "r0_max_cap": 51 }, { "level_index": 3, +"node_depth": 8, +"board_width": 13, +"board_height": 13, +"mine_density": 0.25, +"inferability_target": 0.88, +"solver_rules": "single_subset", +"min_safe_count": 70, +"max_gen_attempts": 32, +"board_type": "deep_underdark", +"shave_ring_radius": 1, +"r0_max_cap": 51 +}, { +"level_index": 3, "node_depth": 9, "board_width": 14, "board_height": 14, @@ -251,6 +355,58 @@ rows = [{ "board_type": "deep_underdark", "shave_ring_radius": 1, "r0_max_cap": 59 +}, { +"level_index": 3, +"node_depth": 10, +"board_width": 14, +"board_height": 14, +"mine_density": 0.25, +"inferability_target": 0.88, +"solver_rules": "single_subset", +"min_safe_count": 82, +"max_gen_attempts": 32, +"board_type": "deep_underdark", +"shave_ring_radius": 1, +"r0_max_cap": 59 +}, { +"level_index": 3, +"node_depth": 11, +"board_width": 14, +"board_height": 14, +"mine_density": 0.25, +"inferability_target": 0.88, +"solver_rules": "single_subset", +"min_safe_count": 82, +"max_gen_attempts": 32, +"board_type": "deep_underdark", +"shave_ring_radius": 1, +"r0_max_cap": 59 +}, { +"level_index": 3, +"node_depth": 12, +"board_width": 14, +"board_height": 14, +"mine_density": 0.25, +"inferability_target": 0.88, +"solver_rules": "single_subset", +"min_safe_count": 82, +"max_gen_attempts": 32, +"board_type": "deep_underdark", +"shave_ring_radius": 1, +"r0_max_cap": 59 +}, { +"level_index": 3, +"node_depth": 13, +"board_width": 14, +"board_height": 14, +"mine_density": 0.25, +"inferability_target": 0.88, +"solver_rules": "single_subset", +"min_safe_count": 82, +"max_gen_attempts": 32, +"board_type": "deep_underdark", +"shave_ring_radius": 1, +"r0_max_cap": 59 }] ; 切片调参登记(SLICE-TUNED provisional,调参所有权仍归 C3): diff --git a/game/data/tables/enemy.tres b/game/data/tables/enemy.tres index 615ca76b..e8c5c1de 100644 --- a/game/data/tables/enemy.tres +++ b/game/data/tables/enemy.tres @@ -99,6 +99,31 @@ rows = [{ "armor_class": 2 }, { "level_index": 1, +"row_kind": "battle", +"depth_band": 6, +"enemy_id": "bugbear", +"display_name": "熊地精", +"unit_count": 1, +"unit_count_max": 2, +"hp": 30, +"atk": 1, +"atk_min": 0, +"atk_max": 2, +"pool": [{ +"skill_id": "attack", +"weight": 6 +}, { +"skill_id": "charge", +"weight": 2 +}, { +"skill_id": "heavy", +"weight": 2 +}], +"attack_type": "normal", +"armor_type": "light", +"armor_class": 4 +}, { +"level_index": 1, "row_kind": "boss", "depth_band": 4, "enemy_id": "ogre_chief", @@ -126,6 +151,34 @@ rows = [{ "armor_type": "light", "armor_class": 8 }, { +"level_index": 1, +"row_kind": "boss", +"depth_band": 6, +"enemy_id": "ogre_chief", + "display_name": "食人魔酋长", + "unit_count": 1, + "hp": 88, + "atk": 1, + "atk_min": 0, + "atk_max": 2, +"pool": [{ +"skill_id": "attack", +"weight": 6 +}, { +"skill_id": "heavy", +"weight": 2 +}, { +"skill_id": "harden", +"weight": 2 +}, { +"skill_id": "quake_smash", +"weight": 2, +"unlock_hp_pct": 50 +}], +"attack_type": "normal", +"armor_type": "light", +"armor_class": 8 +}, { "level_index": 2, "row_kind": "battle", "depth_band": 0, @@ -308,6 +361,65 @@ rows = [{ "armor_class": 3 }, { "level_index": 2, +"row_kind": "battle", +"depth_band": 9, + "enemy_id": "giant_spider", + "display_name": "巨蜘蛛巢群", + "unit_count": 2, + "unit_count_max": 3, + "hp": 40, + "atk": 2, +"atk_min": 1, +"atk_max": 3, +"pool": [{ +"skill_id": "attack", +"weight": 6 +}, { +"skill_id": "bite", +"weight": 2 +}, { +"skill_id": "charge", +"weight": 1 +}, { +"skill_id": "venom_spit", +"weight": 2 +}], +"attack_type": "normal", +"armor_type": "light", +"armor_class": 3 +}, { +"level_index": 2, +"row_kind": "boss", +"depth_band": 8, + "enemy_id": "drider", + "display_name": "蛛化卓尔精灵", + "unit_count": 1, + "hp": 112, + "atk": 3, + "atk_min": 2, + "atk_max": 4, +"pool": [{ +"skill_id": "attack", +"weight": 6 +}, { +"skill_id": "heavy", +"weight": 2 +}, { +"skill_id": "venom_burst", +"weight": 2 +}, { +"skill_id": "web_snare", +"weight": 2 +}, { +"skill_id": "venom_curb", +"weight": 2, +"unlock_hp_pct": 50 +}], +"attack_type": "normal", +"armor_type": "ward", +"armor_class": 10 +}, { +"level_index": 2, "row_kind": "boss", "depth_band": 9, "enemy_id": "drider", @@ -495,6 +607,34 @@ rows = [{ "armor_class": 4 }, { "level_index": 3, +"row_kind": "battle", +"depth_band": 13, +"enemy_id": "duergar", + "display_name": "灰矮人小队", + "unit_count": 2, + "unit_count_max": 3, + "hp": 42, + "atk": 3, + "atk_min": 2, + "atk_max": 4, +"pool": [{ +"skill_id": "attack", +"weight": 6 +}, { +"skill_id": "charge", +"weight": 2 +}, { +"skill_id": "heavy", +"weight": 2 +}, { +"skill_id": "shield_bash", +"weight": 2 +}], +"attack_type": "normal", +"armor_type": "heavy", +"armor_class": 4 +}, { +"level_index": 3, "row_kind": "boss", "depth_band": 9, "enemy_id": "duergar_king", @@ -521,6 +661,90 @@ rows = [{ "attack_type": "normal", "armor_type": "heavy", "armor_class": 12 +}, { +"level_index": 3, +"row_kind": "boss", +"depth_band": 11, + "enemy_id": "duergar_king", + "display_name": "灰矮人石君王", + "unit_count": 1, + "hp": 150, + "atk": 3, + "atk_min": 2, + "atk_max": 4, +"pool": [{ +"skill_id": "attack", +"weight": 6 +}, { +"skill_id": "heavy", +"weight": 2 +}, { +"skill_id": "earth_pulse", +"weight": 2 +}, { +"skill_id": "rockslide", +"weight": 2, +"unlock_hp_pct": 50 +}], +"attack_type": "normal", +"armor_type": "heavy", +"armor_class": 12 +}, { +"level_index": 3, +"row_kind": "boss", +"depth_band": 12, + "enemy_id": "duergar_king", + "display_name": "灰矮人石君王", + "unit_count": 1, + "hp": 162, + "atk": 3, + "atk_min": 2, + "atk_max": 4, +"pool": [{ +"skill_id": "attack", +"weight": 6 +}, { +"skill_id": "heavy", +"weight": 2 +}, { +"skill_id": "earth_pulse", +"weight": 2 +}, { +"skill_id": "rockslide", +"weight": 2, +"unlock_hp_pct": 50 +}], +"attack_type": "normal", +"armor_type": "heavy", +"armor_class": 12 +}, { +"level_index": 3, +"row_kind": "boss", +"depth_band": 13, + "enemy_id": "duergar_king", + "display_name": "灰矮人石君王", + "unit_count": 1, + "hp": 176, + "atk": 3, + "atk_min": 2, + "atk_max": 4, +"pool": [{ +"skill_id": "attack", +"weight": 6 +}, { +"skill_id": "heavy", +"weight": 2 +}, { +"skill_id": "earth_pulse", +"weight": 2 +}, { +"skill_id": "rockslide", +"weight": 2, +"unlock_hp_pct": 50 +}], +"attack_type": "normal", +"armor_type": "heavy", +"armor_class": 12 }] ; 切片调参登记 v3(2026-09-12 试玩批次 1 触发——F7 流程;C3 GDD F5 对照): diff --git a/game/data/tables/level.tres b/game/data/tables/level.tres index ee4a96aa..739e83cb 100644 --- a/game/data/tables/level.tres +++ b/game/data/tables/level.tres @@ -6,20 +6,11 @@ script = ExtResource("1") info_visible_layers = 2 rows = [ -{"level_index": 1, "display_name": "第一层 · 地精洞窟", "rest_heal_pct": 30, "templates": [{"display_name": "地精洞窟·主线洞道(现行)", "nodes": [{"node_depth": 0, "type": "battle"}, {"node_depth": 1, "type": "battle"}, {"node_depth": 2, "type": "battle"}, {"node_depth": 3, "type": "battle"}, {"type": "reward"}, {"type": "rest"}, {"node_depth": 4, "type": "boss"}]}, {"display_name": "地精洞窟·先赏后整", "nodes": [{"node_depth": 0, "type": "battle"}, {"node_depth": 1, "type": "battle"}, {"node_depth": 2, "type": "battle"}, {"type": "reward"}, {"type": "rest"}, {"node_depth": 3, "type": "battle"}, {"node_depth": 4, "type": "boss"}]}, {"display_name": "地精洞窟·先整后赏", "nodes": [{"node_depth": 0, "type": "battle"}, {"node_depth": 1, "type": "battle"}, {"node_depth": 2, "type": "battle"}, {"type": "rest"}, {"type": "reward"}, {"node_depth": 3, "type": "battle"}, {"node_depth": 4, "type": "boss"}]}, {"display_name": "地精洞窟·赏战整", "nodes": [{"node_depth": 0, "type": "battle"}, {"node_depth": 1, "type": "battle"}, {"node_depth": 2, "type": "battle"}, {"type": "reward"}, {"node_depth": 3, "type": "battle"}, {"type": "rest"}, {"node_depth": 4, "type": "boss"}]}, {"display_name": "地精洞窟·整战赏", "nodes": [{"node_depth": 0, "type": "battle"}, {"node_depth": 1, "type": "battle"}, {"node_depth": 2, "type": "battle"}, {"type": "rest"}, {"node_depth": 3, "type": "battle"}, {"type": "reward"}, {"node_depth": 4, "type": "boss"}]}]}, -{"level_index": 2, "display_name": "第二层 · 幽暗地域", "rest_heal_pct": 30, "templates": [{"display_name": "宽廊双干(现行)", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3, 4]}, {"node_depth": 2, "type": "reward", "next": [5, 6]}, {"node_depth": 3, "type": "rest", "next": [7]}, {"node_depth": 4, "type": "battle", "next": [7]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [7]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "三干中廊", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3, 5]}, {"node_depth": 2, "type": "reward", "next": [4, 5]}, {"node_depth": 3, "type": "rest", "next": [6]}, {"node_depth": 4, "type": "battle", "next": [6, 7]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [8]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "宽廊斜岔", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3, 5]}, {"node_depth": 2, "type": "reward", "next": [4, 6]}, {"node_depth": 3, "type": "rest", "next": [7]}, {"node_depth": 4, "type": "battle", "next": [7]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [7]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "三干宽门", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3, 4]}, {"node_depth": 2, "type": "reward", "next": [4, 5]}, {"node_depth": 3, "type": "rest", "next": [6, 7]}, {"node_depth": 4, "type": "battle", "next": [7]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [8]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "窄廊短支", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3, 4]}, {"node_depth": 2, "type": "reward", "next": [5]}, {"node_depth": 3, "type": "rest", "next": [5, 6]}, {"node_depth": 4, "type": "battle", "next": [6]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [7]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}]}, -{"level_index": 3, "display_name": "第三层 · 灰矮人矿城", "rest_heal_pct": 30, "templates": [{"display_name": "宽廊双干(现行)", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3, 4]}, {"node_depth": 2, "type": "reward", "next": [5, 6]}, {"node_depth": 3, "type": "rest", "next": [7]}, {"node_depth": 4, "type": "battle", "next": [7]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [7]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "三干中廊", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3, 5]}, {"node_depth": 2, "type": "reward", "next": [4, 5]}, {"node_depth": 3, "type": "rest", "next": [6]}, {"node_depth": 4, "type": "battle", "next": [6, 7]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [8]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "宽廊斜岔", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3, 5]}, {"node_depth": 2, "type": "reward", "next": [4, 6]}, {"node_depth": 3, "type": "rest", "next": [7]}, {"node_depth": 4, "type": "battle", "next": [7]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [7]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "三干宽门", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3, 4]}, {"node_depth": 2, "type": "reward", "next": [4, 5]}, {"node_depth": 3, "type": "rest", "next": [6, 7]}, {"node_depth": 4, "type": "battle", "next": [7]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [8]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "窄廊短支", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3, 4]}, {"node_depth": 2, "type": "reward", "next": [5]}, {"node_depth": 3, "type": "rest", "next": [5, 6]}, {"node_depth": 4, "type": "battle", "next": [6]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [7]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}]} +{"level_index": 1, "display_name": "第一层 · 地精洞窟", "rest_heal_pct": 30}, +{"level_index": 2, "display_name": "第二层 · 幽暗地域", "rest_heal_pct": 30}, +{"level_index": 3, "display_name": "第三层 · 灰矮人矿城", "rest_heal_pct": 30} ] -; 关卡节点图生成 v0.4 台阶一内容批(M-3 迭代 5 T5,2026-09-28——15 张骨架 = 3 关 × 5,拍板点④): -; L2/L3 网状图约束(承袭):战斗节点编号恒 ∈ 遭遇表行集 {0,1,4,5,6,7}、Boss 9 最大编号、 -; 商店房 8→Boss 必经位承袭;遭遇表按编号共用——零新增敌人数值。L1 五模板 = 单链排列变体 -; (战斗深度序单调保难度坡,变化面 = 奖励/恢复位次)。 -; 池内全模板加载期校验(level_table.gd validate + config_db 跨表校验模板感知); -; 行为读 LevelRunner._resolve_template_row(node_map 标签恰一 scope 抽签 / template_pick 强制)。 -; 生成器 = tools/t5_build_templates.py(生成前自校验,一次性内容批工具随批入库留档)。 -; U2 目检返工(M-3 迭代 5 返工批,2026-09-29——U2 目检 15/15 不过两规则缺口,全池重生成): -; ① L1 奖励/休整禁入前三位(用户目检裁决)——五链全部前三位纯战斗,位 4-6 = {战斗3,奖励,休整} 排列; -; ② L2/L3 BFS 同深度层上限 5 且应用足(每张最大层宽 4-5,旧池层宽最大 3 被否);模板 0 一并重排 -; (待批点甲按推荐=随新规则重生成——首次冒险/钉固断言期望随新图更新;模板 0 名保持「(现行)」后缀=新现行图锚)。 -; 加载校验两规则同落 level_table.gd validate(单链前三纯战斗/图形态层宽 ≤5)。 +; 节点图源已换代为参数化生成器(关卡节点图生成 v1.1)——本表逐关行只承载关卡序号/显示名/恢复比例, +; 不再携带节点数据;图 = f(参数档, 种子),参数档见 level_gen_params.tres 三行(消费侧按 level_index 解析后交 generate)。 +; 旧 15 张模板池(3 关 × 5)随迭代 35 卡② 清出,历史查 git 提交史与 tools/t5_build_templates.py、t17_fanout2_rebuild.py 留档。 diff --git a/game/data/tables/settlement.tres b/game/data/tables/settlement.tres index 8bf9a0c9..dd906c13 100644 --- a/game/data/tables/settlement.tres +++ b/game/data/tables/settlement.tres @@ -11,7 +11,7 @@ segment_order = PackedStringArray("grant", "buff_debuff", "combat", "decay", "ve source_label_map = { "safe_cell": "安全格", "seal_hit": "封印命中", -"mine_hit": "踩雷", +"mine_hit": "危险格", "clear_reward": "清图奖励", "enemy_action": "敌方行动", "hero_attack": "主角攻击", diff --git a/game/presentation/battle_narrator.gd b/game/presentation/battle_narrator.gd index 1422f766..c765c3ea 100644 --- a/game/presentation/battle_narrator.gd +++ b/game/presentation/battle_narrator.gd @@ -139,7 +139,7 @@ static func bonus_result_text(result: Dictionary) -> String: # 表无 archetype,fallback 兜底词「流派」会实显僵尸文案),卡片=件名+效果摘要两行) static func feat_card_text(row: Dictionary) -> String: if row.is_empty() or String(row.get("feat_id", "")) == "": - return "特性\n???" + return "专长\n???" var summary := String(row.get("desc", "")) if summary != "": return "%s\n%s" % [String(row.get("display_name", row.get("feat_id", ""))), summary] @@ -151,9 +151,9 @@ static func bonus_keystone_line(payload: Dictionary) -> String: return "钥石 +%d · 累计 %d" % [int(payload.get("keystone_gained", 0)), int(payload.get("keystone_total", 0))] -# 奖励节点三选一头部行(与通关三选一共享同池;node-reward.md v2) +# 奖励节点三选一头部行(与通关三选一共享同库;渠道白名单分流——node-reward.md v2) static func node_curio_header() -> String: - return "关卡奖励点\n从提升池抽三选一(与通关三选一同池同权重同上限):\n选择一件带走:" + return "关卡奖励点\n从冒险牌库抽三选一(属性值提升与补给;与通关专长三选一同库分流):\n选择一件带走:" # 冒险结算屏文案(摘要五件事:打穿没有 / 通关几关 / 止步第几关 / 钥石合计 / 主种子) diff --git a/game/presentation/main.gd b/game/presentation/main.gd index 2ea655c9..11eb50ed 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -589,11 +589,9 @@ func _start_adventure() -> void: logger.start_session(seed_val) run_scope.adventure.set_hero_override(debug_overlay.atk_pct(), debug_overlay.hp_pct()) # 永久强化注入:冒险开始参数面——meta 现算 override 显式传参(全零 = 白板) - # 首次冒险脚本(关卡节点图生成 v0.4):存档布尔位 false = 全关固定模板 0(教学重放);置位后逐关抽签 - # 同调三键随开局下传(build_adventure_params 统一构造,与营地投影同源) - var first_script: bool = not save_manager.is_first_adventure_done() + # 开局参数面(同调三键同源构造——确定性承载=固定种子+参数档,首局教学图由种子域三分判定给 42) if not run_scope.adventure.start_adventure(seed_val, build_adventure_params(save_manager, meta, - {"first_script": first_script, "treasure_unlocked": _treasure_ui.unlocked_set(), "shop_mode": "pool"})): + {"treasure_unlocked": _treasure_ui.unlocked_set(), "shop_mode": "pool"})): _fatal_screen("冒险启动失败——见输出日志") return @@ -604,12 +602,16 @@ func _on_adventure_level_started(level_index: int, display_name: String) -> void run_scope.runner = run_scope.adventure.runner() # 开场行传当前生效值(runner.effective_atk/effective_max_hp 与战斗面板同源: # 表值+永久强化 flat+加算区 flat 后乘调试浮窗 pct,本回调时点 runner 已带全部加成);「含面板加成」 - # 后缀判断认局外加成——永久强化 atk/max_hp flat 或调试浮窗 pct 任一存在才带,白板局不带 + # 后缀判断认任一加成源——永久强化 atk/max_hp flat 或调试浮窗 pct 或局内加算区 atk/max_hp + #(run_ledger.bonus——属性值提升入账后显示值已含加算区 flat,注记随动;armor_class/ap 不入条件: + # 开场行只显示 atk/max_hp 两值,注记指向不存在的显示轴反成失真),白板局不带 var ov: Dictionary[Variant, Variant] = meta.hero_override() + var boosted: bool = int(ov.get("atk", 0)) > 0 or int(ov.get("max_hp", 0)) > 0 \ + or debug_overlay.atk_pct() > 0 or debug_overlay.hp_pct() > 0 \ + or int(run_scope.runner.run_ledger.bonus.get("atk", 0)) > 0 \ + or int(run_scope.runner.run_ledger.bonus.get("max_hp", 0)) > 0 log_view.append_bb(Narrator.level_open_line(display_name, int(run_scope.adventure.master_seed), - run_scope.runner.effective_atk(), run_scope.runner.effective_max_hp(), - int(ov.get("atk", 0)) > 0 or int(ov.get("max_hp", 0)) > 0 - or debug_overlay.atk_pct() > 0 or debug_overlay.hp_pct() > 0)) + run_scope.runner.effective_atk(), run_scope.runner.effective_max_hp(), boosted)) func _on_runner_node(node: Dictionary, info: Dictionary) -> void: @@ -1064,18 +1066,34 @@ func _on_settings_closed() -> void: # ---- 局内状态面板·只读(入口角标在两屏装配段;开合与数据喂入归组合根)---- -# 状态面板快照投影(数据只读同源:每次开层实时读 MetaProgression 访问器现值拼装—— +# 状态面板快照投影(数据只读同源:每次开层实时读访问器现值拼装—— # 快照仅服务单次开层生命周期(open_status 重建各行),跨开层零持有;零写调用。 -# 状态面板零锻造段) +# 八键换代(迭代 36 卡③):跨关解锁概览退役(营地/生涯承载)——只留本局信息; +# ledger 直取下标(键缺失=致命,禁 .get 缺省留门——fail-fast 红线);禁 null 守卫软回退 +# (两入口开层时 runner 恒在役——状态面板卡 F3 证据链) func _status_data() -> Dictionary: return { "balance": meta.keystone_balance(), - "workshop": meta.treasure_shop_status(), # 秘宝匣 12 件解锁投影 - "tree": meta.tree_camp_rows(), - "fork_groups": meta.tree_fork_groups(), + "hero_hp": int(run_scope.runner.hero_hp), # LevelRunner 属性面(_ledger 直取下标——键缺失=致命) + "max_hp": run_scope.runner.effective_max_hp(), + "atk": run_scope.runner.effective_atk(), + "ac": run_scope.runner.effective_ac(), + "gp": int(run_scope.runner.gp), + "curios": _status_curios(), + "feats": HudPresenter.feat_holdings_lines(db, run_scope.runner.feats_held()), } +# 秘宝行映射(inventory_projection 现役投影逐行取 name/used 两键——受理序沿库存投影; +# 禁第二份投影:件名换算已在 inventory_projection 收口,此处只做键裁剪) +func _status_curios() -> Array: + var out: Array[Variant] = [] + for row in (_treasure_ui.inventory_projection() as Array): + var r: Dictionary[Variant, Variant] = row + out.append({"name": String(r.get("name", "")), "used": bool(r.get("used", false))}) + return out + + func _open_status_panel() -> void: _open_modal(status_panel) # 模态栈单口(开层前排空排播队列——既有层同款行为,目检确认项) status_panel.open_status(_status_data()) diff --git a/game/presentation/views/board_cell_unit.gd b/game/presentation/views/board_cell_unit.gd index 31d1ac24..7feda330 100644 --- a/game/presentation/views/board_cell_unit.gd +++ b/game/presentation/views/board_cell_unit.gd @@ -267,7 +267,8 @@ func _draw() -> void: elif fog_state == 2: # 记忆态换装(demo 口径「地板纹理 + 灰罩 α.66」: # 地板纹理原样直画 + FOG_MEMORY_PLATE α.66 灰罩压顶;素材零改动、格边长零改动。 - # 记忆墨 FOG_MEMORY_INK 对合成底实测 ≈6.7:1 ≥4.5:1(§7.2 硬指标;floor avg #453020 复算在册)) + # 记忆墨 FOG_MEMORY_INK 对合成底归一后(真实渲染面)三关 5.71–5.93:1 ≥4.5:1(§7.2 硬指标; + # 测点行值与素材随动,见 ui_contrast_probe 迷雾段)) if tex_base != null: draw_texture_rect(tex_base, Rect2(Vector2.ZERO, Vector2(edge, edge)), false) draw_rect(Rect2(Vector2.ZERO, Vector2(edge, edge)), Color(UiKit.FOG_MEMORY_PLATE, 0.66), true) diff --git a/game/presentation/views/board_view.gd b/game/presentation/views/board_view.gd index 88e2d230..976a2006 100644 --- a/game/presentation/views/board_view.gd +++ b/game/presentation/views/board_view.gd @@ -727,7 +727,7 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void: var rec: Dictionary[Variant, Variant] = _minesweeper.reveal_record(c) # 已开记录经查询面 if rec.get("kind", "") == "mine": bg = COL_FLOOR - glyph = "雷" + glyph = "危" digit_col = UiKit.RED_MINE else: bg = COL_FLOOR @@ -773,13 +773,13 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void: u.bg_sb = sb u.glyph = glyph u.glyph_color = digit_col - # 色牌后备(数字直画退牌;雷/「?」维持牌载保读性)+ v3.4:双峰纹理板 + # 色牌后备(数字直画退牌;「危」/「?」维持牌载保读性)+ v3.4:双峰纹理板 #(_digit_plate——underdark 系亮白筋+近黑斑并存的直画不可救纹理)数字落牌,牌上深墨 12:1 - var plate_digit := _digit_plate and glyph != "" and glyph != "雷" and glyph != "?" and glyph != "封" and glyph != "人" + var plate_digit := _digit_plate and glyph != "" and glyph != "危" and glyph != "?" and glyph != "封" and glyph != "人" if plate_digit: digit_col = COL_DIGIT # 牌上深墨族(素牌 12:1——双峰纹理免疫) u.glyph_color = digit_col - u.plate = revealed and (glyph == "雷" or glyph == "?" or plate_digit) + u.plate = revealed and (glyph == "危" or glyph == "?" or plate_digit) u.outline_sides.assign([]) # typed Array[int](assign 保持引用语义) u.facade_south = false u.proj_band = 0.0 diff --git a/game/presentation/views/board_zone_view.gd b/game/presentation/views/board_zone_view.gd index 4c2d6861..68573246 100644 --- a/game/presentation/views/board_zone_view.gd +++ b/game/presentation/views/board_zone_view.gd @@ -55,7 +55,7 @@ func _init() -> void: corner.add_theme_constant_override("separation", 8) status_badge = UiKit.button("状态", 0.0) # 角落小件豁免宽度基准 status_badge.add_theme_font_size_override("font_size", UiKit.FS_HINT) - status_badge.tooltip_text = "查看钥石与永久强化进度(只读)" + status_badge.tooltip_text = "查看本局冒险进度(只读)" status_badge.visible = false # 显式初始化——非对局期不误现(可见性归组合根同步) corner.add_child(status_badge) corner_wrap.add_child(corner) diff --git a/game/presentation/views/camp_overlay_view.gd b/game/presentation/views/camp_overlay_view.gd index e039b033..543c0380 100644 --- a/game/presentation/views/camp_overlay_view.gd +++ b/game/presentation/views/camp_overlay_view.gd @@ -164,7 +164,7 @@ func _init() -> void: atlas_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL atlas_panel.size_flags_vertical = Control.SIZE_FILL # 四栏等高:任一栏长高时四栏同步拉伸 mid.add_child(atlas_panel) - var lock_l := UiKit.label(" —— 柜台结构位预留:内容排 Alpha(地图战斗资源差异化批)",UiKit.FS_HINT, false, UiKit.STONE) + var lock_l := UiKit.label(" —— 柜台结构位预留:内容排 Alpha(地图战斗资源差异化批)",UiKit.FS_HINT, false, UiKit.STONE_DEEP) lock_l.size_flags_vertical = Control.SIZE_SHRINK_CENTER # 锁文在等高栏内垂直居中 lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART lock_l.custom_minimum_size = Vector2(280, 0) @@ -414,7 +414,7 @@ func refresh_camp(data: Dictionary) -> void: card.progress_l.text = prog_text # 状态色用亮板双色墨制(深墨族替代暖白族——亮板上暖白不可读) var st_text := "未解锁" - var st_col: Color = UiKit.STONE + var st_col: Color = UiKit.STONE_DEEP if unlocked: st_text = "已解锁" st_col = UiKit.GOLD_INK # GOLD_DEEP 对行板 3.00:1 越带,换深金墨 5.84:1 @@ -473,7 +473,7 @@ func _rebuild_workshop(data: Dictionary) -> void: progress_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS line.add_child(progress_l) # 状态色用亮板双色墨制(深墨族替代暖白族)——色随刷新态条件赋值,非机械两行式 - var status_l := UiKit.label("",UiKit.FS_HINT, true, UiKit.STONE) + var status_l := UiKit.label("",UiKit.FS_HINT, true, UiKit.STONE_DEEP) status_l.clip_text = true # 同上——「已解锁 · 能力封存中」不撑柜(余宽吸收者) status_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS status_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL @@ -506,11 +506,11 @@ func _rebuild_attune(data: Dictionary) -> void: name_l.clip_text = true name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS line.add_child(name_l) - var form_l := UiKit.label("被动" if String(row.get("form", "")) == "passive" else "主动·充能",UiKit.FS_CAPTION, false, UiKit.STONE) + var form_l := UiKit.label("被动" if String(row.get("form", "")) == "passive" else "主动·充能",UiKit.FS_CAPTION, false, UiKit.STONE_DEEP) form_l.custom_minimum_size = Vector2(58, 0) line.add_child(form_l) # 档位标签(槽位档位显示:件档=普通/优秀/稀有,装入须件档≤格档契约 2 的呈现半边) - var tier_l := UiKit.label(String(UiKit.TREASURE_TIER_LABELS.get(String(row.get("tier", "")), "—")),UiKit.FS_CAPTION, false, UiKit.STONE) + var tier_l := UiKit.label(String(UiKit.TREASURE_TIER_LABELS.get(String(row.get("tier", "")), "—")),UiKit.FS_CAPTION, false, UiKit.STONE_DEEP) tier_l.custom_minimum_size = Vector2(36, 0) line.add_child(tier_l) var btn := UiKit.button("装配", 0.0) # 行板紧凑钮豁免 @@ -522,7 +522,7 @@ func _rebuild_attune(data: Dictionary) -> void: _attune_btns.append({"item_id": iid, "btn": btn, "unlocked": bool(row.get("unlocked", false)), "equipped": bool(row.get("equipped", false))}) var refresh_line := HBoxContainer.new() refresh_line.add_theme_constant_override("separation", 6) - _refresh_l = UiKit.label("",UiKit.FS_CAPTION, false, UiKit.STONE) + _refresh_l = UiKit.label("",UiKit.FS_CAPTION, false, UiKit.STONE_DEEP) refresh_line.add_child(_refresh_l) _refresh_btn = UiKit.button("升级", 0.0) # 行板紧凑钮豁免 _refresh_btn.pressed.connect(_emit_buy_requested.bind("attune_refresh")) diff --git a/game/presentation/views/hero_info_panel_view.gd b/game/presentation/views/hero_info_panel_view.gd index a7465d05..bc020fff 100644 --- a/game/presentation/views/hero_info_panel_view.gd +++ b/game/presentation/views/hero_info_panel_view.gd @@ -70,7 +70,7 @@ func _init() -> void: _feat_section.add_theme_constant_override("separation", 4) _feat_section.visible = false col.add_child(_feat_section) - _feat_section.add_child(UiKit.label("特性",UiKit.FS_BODY, true)) + _feat_section.add_child(UiKit.label("专长",UiKit.FS_BODY, true)) _feat_list = VBoxContainer.new() _feat_list.add_theme_constant_override("separation", 2) _feat_section.add_child(_feat_list) @@ -90,7 +90,7 @@ func _init() -> void: _curio_section.add_theme_constant_override("separation", 4) _curio_section.visible = false col.add_child(_curio_section) - _curio_section.add_child(UiKit.label("特性",UiKit.FS_BODY, true)) + _curio_section.add_child(UiKit.label("属性值提升",UiKit.FS_BODY, true)) _curio_list = VBoxContainer.new() _curio_list.add_theme_constant_override("separation", 2) _curio_section.add_child(_curio_list) diff --git a/game/presentation/views/legend_overlay.gd b/game/presentation/views/legend_overlay.gd index 45f979e0..08454647 100644 --- a/game/presentation/views/legend_overlay.gd +++ b/game/presentation/views/legend_overlay.gd @@ -8,7 +8,7 @@ signal closed const ENTRIES := [ ["#C9AF83", "完整覆尘地砖", "未开格——左键挖开(1AP,须相邻)· 右键封印(1AP)"], ["#B39A78", "碎裂裸土", "已开 0 格——可通行,左键移动(免费)"], - ["#000000", "封印", "封印印记(呈现名·不能拔)——轮末自动判定标中;插错可左键直接挖开(安全格零奖励、危险格吃挖雷惩罚)"], + ["#000000", "封印", "封印印记(呈现名·不能拔)——轮末自动判定标中;标空可左键直接挖开(安全格零奖励、危险格受挖掘惩罚)"], ["#2e86de", "小人", "探险者——8 向相邻未开格常驻微光 = 可挖域"], ["#F0A828", "金徽章", "已开格奖励身份 icon(封印之下、数值之上,不进数字区)"], ["#F0A828", "九徽章解码", "攻=攻击 · 守=护甲 · 爆=爆裂 · 医=回复 · 弱=削弱 · 穿=穿刺 · 魔=魔法 · 行=行动点 · 金=GP"], diff --git a/game/presentation/views/level_map_screen.gd b/game/presentation/views/level_map_screen.gd index d0516d07..bd95e641 100644 --- a/game/presentation/views/level_map_screen.gd +++ b/game/presentation/views/level_map_screen.gd @@ -104,7 +104,7 @@ func _init() -> void: # 信号连接归组合根(视图零连接)) status_badge = UiKit.button("状态", 0.0) status_badge.add_theme_font_size_override("font_size", UiKit.FS_HINT) - status_badge.tooltip_text = "查看钥石与永久强化进度(只读)" + status_badge.tooltip_text = "查看本局冒险进度(只读)" status_badge.size_flags_vertical = Control.SIZE_SHRINK_CENTER top.add_child(status_badge) _title = UiKit.label(TITLE_FORK,UiKit.FS_SUB, true, UiKit.WARM) @@ -168,7 +168,7 @@ func _init() -> void: _bonus_row.add_child(bonus_box) var bonus_inner := VBoxContainer.new() bonus_box.add_child(bonus_inner) - var bonus_cap := UiKit.label("特性",UiKit.FS_CAPTION, false, UiKit.INK_HINT) # 深墨(亮板可读) + var bonus_cap := UiKit.label("属性值提升",UiKit.FS_CAPTION, false, UiKit.INK_HINT) # 深墨(亮板可读) bonus_inner.add_child(bonus_cap) _bonus_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_TITLE) # 淡墨→深墨(同上) bonus_inner.add_child(_bonus_value) diff --git a/game/presentation/views/status_panel.gd b/game/presentation/views/status_panel.gd index 5976d3ae..e97ac37f 100644 --- a/game/presentation/views/status_panel.gd +++ b/game/presentation/views/status_panel.gd @@ -1,14 +1,16 @@ ## VERTICAL SLICE - NOT FOR PRODUCTION -## 局内状态面板·只读:钥石余额与进度概览(入口=两屏角标,本类不含入口)。 +## 局内状态面板·只读:本局信息面板(钥石总量+本局四件:带的秘宝/已获专长/当前属性/现有 GP)。 +## 跨关解锁概览(秘宝匣解锁态/天赋树已购)由营地/生涯承载,本面板不镜像。 ## 账面快照由组合根 open_status 喂入;进出零动效直开直关;构成件全部经 UiKit 工厂产出——禁裸色值自建样式。 class_name StatusPanel extends Control signal closed -var _balance_badge: UiKit.KeystoneBadge # 钥石余额徽(badge 工厂件——Q10 甲) -var _workshop_holder: VBoxContainer -var _tree_label: Label +var _balance_badge: UiKit.KeystoneBadge # 钥石总量徽(badge 工厂件——Q10 甲) +var _attrs_label: Label +var _curios_label: Label +var _feats_label: Label func _init() -> void: @@ -19,11 +21,11 @@ func _init() -> void: var title := UiKit.label("状态",UiKit.FS_TITLE, true, UiKit.INK_TITLE) title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER box.add_child(title) - var hint := UiKit.label("查看本局钥石与永久强化进度(只读——购买请回营地)",UiKit.FS_HINT, false, UiKit.INK_HINT) + var hint := UiKit.label("查看本局冒险进度(只读)",UiKit.FS_HINT, false, UiKit.INK_HINT) hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER box.add_child(hint) - # 钥石余额行(badge 工厂件——与营地顶栏钥石徽同形制) + # 钥石总量行(badge 工厂件——与营地顶栏钥石徽同形制) var bal_row := HBoxContainer.new() bal_row.alignment = BoxContainer.ALIGNMENT_CENTER box.add_child(bal_row) @@ -31,13 +33,20 @@ func _init() -> void: bal_row.add_child(_balance_badge) box.add_child(HSeparator.new()) - _section_head(box, "道具工坊 · 解锁态") - _workshop_holder = _section_holder(box) + _section_head(box, "本局状态") + _attrs_label = UiKit.label("",UiKit.FS_HINT, false, UiKit.INK_LABEL) + _attrs_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + box.add_child(_attrs_label) - _section_head(box, "天赋树 · 已购概览") - _tree_label = UiKit.label("",UiKit.FS_HINT, false, UiKit.INK_LABEL) - _tree_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - box.add_child(_tree_label) + _section_head(box, "带的秘宝") + _curios_label = UiKit.label("",UiKit.FS_HINT, false, UiKit.INK_LABEL) + _curios_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + box.add_child(_curios_label) + + _section_head(box, "已获专长") + _feats_label = UiKit.label("",UiKit.FS_HINT, false, UiKit.INK_LABEL) + _feats_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + box.add_child(_feats_label) box.add_child(HSeparator.new()) var close_row := HBoxContainer.new() @@ -56,80 +65,26 @@ func _section_head(box: VBoxContainer, text: String) -> void: box.add_child(head) -func _section_holder(box: VBoxContainer) -> VBoxContainer: - var holder := VBoxContainer.new() - holder.add_theme_constant_override("separation", 4) - box.add_child(holder) - return holder - - # ---- 开层(组合根唯一调用口——快照随开随建,R4 单次开层生命周期口径)---- - +# 八键快照(组合根 _status_data 换代):balance/hero_hp/max_hp/atk/ac/gp/curios/feats—— +# 本局四件拼装在此,禁第二份投影(秘宝=inventory_projection 行映射、专长=feat_holdings_lines 现成行) func open_status(data: Dictionary) -> void: _balance_badge.set_text("钥石 %d" % int(data.get("balance", 0))) - _rebuild_workshop(data) - _rebuild_tree(data) - visible = true - - -# 道具工坊六行(解锁态文案与营地柜台同口径——退役/封存/已解锁/未解锁;进度=解锁判定量读数) -# 秘宝匣 12 行只读概览(数据源=treasure_shop_status 投影, -# 解锁态文案与营地柜台同口径;进度列=解锁混合制三形态:钥石价/条件进度拼接/已入手) -func _rebuild_workshop(data: Dictionary) -> void: - for c in _workshop_holder.get_children(): - c.queue_free() - for row in (data.get("workshop", []) as Array): - var line := HBoxContainer.new() - line.add_theme_constant_override("separation", 8) - var name_l := UiKit.label(String(row.get("display_name", "")),UiKit.FS_HINT, true, UiKit.INK) - name_l.custom_minimum_size = Vector2(120, 0) - name_l.clip_text = true - name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - line.add_child(name_l) - var unlocked := bool(row.get("unlocked", false)) - var prog_text := "已入手" - if not unlocked: - if String(row.get("mode", "")) == "keystone": - prog_text = "钥石 %d" % int(row.get("price", 0)) - else: - var parts: Array[String] = [] - for req_v in (row.get("reqs", []) as Array): - var req: Dictionary[Variant, Variant] = req_v as Dictionary - var lbl := String(UiKit.TREASURE_REQ_LABELS.get(String(req.get("key", "")), "条件")) - parts.append("%s %d/%d" % [lbl, int(req.get("progress", 0)), int(req.get("threshold", 0))]) - prog_text = "、".join(parts) - var prog_l := UiKit.label(prog_text,UiKit.FS_HINT, false, UiKit.INK_HINT) - prog_l.custom_minimum_size = Vector2(150, 0) - prog_l.clip_text = true - prog_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - line.add_child(prog_l) - var st_l := UiKit.label("已解锁" if unlocked else "未解锁",UiKit.FS_HINT, true, UiKit.INK_LABEL) - st_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL - line.add_child(st_l) - _workshop_holder.add_child(line) - - -# 天赋树已购概览(单条聚合文案——已购节点等级 + Fork 组激活侧;零已购=空态句) -func _rebuild_tree(data: Dictionary) -> void: - var owned: Array[String] = [] - for row in (data.get("tree", []) as Array): - var lvl := int(row.get("level", 0)) - if lvl > 0: - owned.append("%s Lv%d/%d" % [String(row.get("display_name", "")), lvl, int(row.get("cap", 1))]) - for fg in (data.get("fork_groups", []) as Array): - var active := String(fg.get("active_side", "")) - if active == "": - continue - var side_name := active - for sd in (fg.get("sides", []) as Array): - if String(sd.get("side", "")) == active: - side_name = String(sd.get("display_name", active)) - break - owned.append("%s·激活 %s" % [String(fg.get("display_name", "")), side_name]) - if owned.is_empty(): - _tree_label.text = "尚未购买任何节点(天赋树在营地主柜台)" + _attrs_label.text = "生命 %d/%d · 攻击 %d · 护甲 AC %d · GP %d" % [ + int(data.get("hero_hp", 0)), int(data.get("max_hp", 0)), int(data.get("atk", 0)), + int(data.get("ac", 0)), int(data.get("gp", 0))] + var curios: Array[Variant] = data.get("curios", []) + if curios.is_empty(): + _curios_label.text = "未携带秘宝" else: - _tree_label.text = "已购 %d 项:%s" % [owned.size(), " · ".join(owned)] + var parts: Array[String] = [] + for row in curios: + var r: Dictionary[Variant, Variant] = row + parts.append(String(r.get("name", "")) + ("(已用)" if bool(r.get("used", false)) else "")) + _curios_label.text = " · ".join(parts) + var feats: Array[Variant] = data.get("feats", []) + _feats_label.text = "未获专长" if feats.is_empty() else " · ".join(feats) + visible = true func _emit_closed() -> void: diff --git a/game/presentation/views/status_slot_row.gd b/game/presentation/views/status_slot_row.gd index 073aba28..0e1cf51d 100644 --- a/game/presentation/views/status_slot_row.gd +++ b/game/presentation/views/status_slot_row.gd @@ -37,7 +37,7 @@ const EFFECT_SEMANTIC := { "empower": "敌方输出 ×2(enemy_output 池加算——蓄力技施放;持续 1 个有效结算)", "fortify": "敌方防御 −50(enemy_defense 池——硬化技施放,主角攻击 ×0.5;持续 1 个有效结算)", } -const SOURCE_NAMES := {"safe_cell": "安全格", "seal_hit": "封印命中", "mine_hit": "踩雷", "clear_reward": "清图奖励"} +const SOURCE_NAMES := {"safe_cell": "安全格", "seal_hit": "封印命中", "mine_hit": "危险格", "clear_reward": "清图奖励"} # 未知图标键的占位(HUD框架:占位也要可见,禁静默空图标——debug 断言先行,release 回退槽内 ? 文本) const ICON_PLACEHOLDER := "?" diff --git a/game/probe/board_shot_probe.gd b/game/probe/board_shot_probe.gd index b5c7bb67..9b9340d6 100644 --- a/game/probe/board_shot_probe.gd +++ b/game/probe/board_shot_probe.gd @@ -54,7 +54,7 @@ func _ready() -> void: var rng = load("res://core/rng_service.gd").new() rng.set_master_seed(420000 + level_index) var runner = load("res://core/level_runner.gd").new(db, rng) - if not runner.start_level(level_index, 420000 + level_index, {}, 0): + if not runner.start_level(level_index, 420000 + level_index): push_error("[board_shot] 开关失败 level=%d" % level_index) get_tree().quit(1) return diff --git a/game/probe/shot_probe.gd b/game/probe/shot_probe.gd index c74a3e7f..2b108b21 100644 --- a/game/probe/shot_probe.gd +++ b/game/probe/shot_probe.gd @@ -10,7 +10,7 @@ # 每屏双帧 + frame_post_draw 同步后抓 get_viewport().get_texture(),PNG 落输出目录; # 最小自检 = 七件存在且非零字节(像素方差断言归 tools/shot_checker.py——B 档骨架分工)。 # 商店房不纳屏(真实路径口径):真实路径必经 L1 打穿→L2 商店节点 -# 选路(商店层在 L2 Boss 前必经位)——探针现行驱动只进首战节点,低代价确定性 +# 选路(商店按摆放约束恒在终点前一位,每局必经恰一店)——探针现行驱动只进首战节点,低代价确定性 # 到达不成立;合成投喂(ui_struct_probe 同款)破坏本探针真实路径口径 → 不纳屏。 extends Node diff --git a/game/probe/type_glow_shot_probe.gd b/game/probe/type_glow_shot_probe.gd index f7b9c385..73fd27fe 100644 --- a/game/probe/type_glow_shot_probe.gd +++ b/game/probe/type_glow_shot_probe.gd @@ -46,7 +46,7 @@ func _ready() -> void: var rng = load("res://core/rng_service.gd").new() rng.set_master_seed(421001) var runner = load("res://core/level_runner.gd").new(db, rng) - if not runner.start_level(1, 421001, {}, 0): + if not runner.start_level(1, 421001): push_error("[type_glow_shot] 开局失败") get_tree().quit(1) return diff --git a/game/probe/ui_click_probe.gd b/game/probe/ui_click_probe.gd index d64d845d..b57d332c 100644 --- a/game/probe/ui_click_probe.gd +++ b/game/probe/ui_click_probe.gd @@ -26,6 +26,9 @@ var _tested_review := false var _tested_end_review := false # 终局面板回看入口(z 序修复回归) var _tested_refresh_chain := false var _tested_tooltip := false +var _tested_copy := false # 迭代36 卡A——C 区两段头真实入口断言(每 run 一次) +var _tested_curio_head := false # 迭代36 卡A——三选一头部旧话清零断言(奖励点停点捕获) +var _tested_map_cap := false # 迭代36 卡A——地图屏加成帽换代断言(每 run 一次) var _tested_status := false # 状态面板角标开合/守卫断言(每 run 一次) var _tested_map_status := false # 地图屏角标一次性 case(选路态零扰动) var _inter_confirms := 0 # 大奖励屏确认计数(冒险推进停点——首见确认推进 L2,再见 = L2 通关收口) @@ -121,6 +124,54 @@ func _ready() -> void: iso_fk.load_from_disk() if String(iso_fk.fork_side("fg_posture")) != "b": _problems.append("Fork 切态未持久(reload 后激活态 != b)") + # ---- 营地六处灰字取色断言(迭代36 wc4——真实入口开营地后按文本锚取 label, + # font_color 逐处 == STONE_DEEP;六源位 :167 图鉴锁文/:417+:476 行状态字/:509 同调形态/:513 档位标签/:525 刷新行)---- + var camp_labels: Array = [] + _collect_labels_by_text(main.camp_overlay, camp_labels) + var atlas_locks := _labels_with_text(camp_labels, "柜台结构位预留") + if atlas_locks.is_empty(): + _problems.append("营地灰字:图鉴柜台锁文 label 未找到(wc4 定位锚失守)") + for l in atlas_locks: + if l.get_theme_color("font_color") != UiKit.STONE_DEEP: + _problems.append("营地灰字:图鉴锁文未用 STONE_DEEP(wc4 :167)") + # 「未解锁」锚收窄到秘宝匣行容器子树(树行/锻造行也有「未解锁」态字样——非 wc4 目标位) + var locked_status: Array = [] + if main.camp_overlay.get("_workshop_holder") != null: + var ws_labels: Array = [] + _collect_labels_by_text(main.camp_overlay._workshop_holder, ws_labels) + locked_status = _labels_with_text(ws_labels, "未解锁") + if locked_status.is_empty(): + _problems.append("营地灰字:秘宝匣行「未解锁」态 label 未找到(wc4 夹具前提失守——新档应有锁定秘宝行)") + for l in locked_status: + if l.get_theme_color("font_color") != UiKit.STONE_DEEP: + _problems.append("营地灰字:秘宝行「未解锁」态未用 STONE_DEEP(wc4 :417/:476)") + var form_found := false + for l in camp_labels: + if l.text == "被动" or l.text == "主动·充能": + form_found = true + if l.get_theme_color("font_color") != UiKit.STONE_DEEP: + _problems.append("营地灰字:同调形态标签未用 STONE_DEEP(wc4 :509)") + if not form_found: + _problems.append("营地灰字:同调行形态标签未找到(wc4 定位锚失守——同调段应有行)") + var tier_vals := {} + for v in UiKit.TREASURE_TIER_LABELS.values(): + tier_vals[String(v)] = true + var tier_found := false + for l in camp_labels: + if tier_vals.has(l.text): + tier_found = true + if l.get_theme_color("font_color") != UiKit.STONE_DEEP: + _problems.append("营地灰字:同调档位标签未用 STONE_DEEP(wc4 :513)") + if not tier_found: + _problems.append("营地灰字:同调档位标签未找到(wc4 定位锚失守)") + var refresh_inked := false + for l in camp_labels: + if String(l.text).contains("三选一刷新"): + refresh_inked = true + if l.get_theme_color("font_color") != UiKit.STONE_DEEP: + _problems.append("营地灰字:刷新行未用 STONE_DEEP(wc4 :525)") + if not refresh_inked: + _problems.append("营地灰字:刷新行 label 未找到(wc4 定位锚失守)") main._on_camp_closed() await get_tree().process_frame main._on_save_select_closed() # 关闭被栈还原的选档层(动线序中间态——卡④ 换代点) @@ -322,6 +373,12 @@ func _ready() -> void: var t1 := Time.get_ticks_msec() main._start_adventure_from_menu() await get_tree().process_frame + await get_tree().process_frame + # 既有口径回归(迭代36 wc3——调试滑条 400/200=局外加成 pct>0,开场行应带「(含面板加成)」; + # 本局为本探针首个冒险,日志无先前行,全文本判即可) + var wc3_txt: String = main.log_view._log.get_parsed_text() + if not wc3_txt.contains("种子 42") or not wc3_txt.contains("(含面板加成)"): + _problems.append("开场行回归失守:局外加成(pct>0)局的开场行应带「(含面板加成)」(wc3)") # 关卡开始 = 入口选路停点——press_candidate 图上命中路径选入口候选进首战 #(首格可点 AC 口径随之含入口选路一步:菜单点击 → 停点呈现 → pick → 棋盘安装) if main.level_screen.visible: @@ -358,6 +415,12 @@ func _ready() -> void: # 未适配则走到即卡死)。两态分派:result 态(兑付反馈——点继续推进)/ # 待选态(点首件+确认)。真实信号路径全线。〔无替换子态——装配不可变契约。〕 # 三选一停点输入锁定断言(停点态断言面:待选/兑付两态共用此层)。 + if not _tested_curio_head and main.run_scope.adventure != null and not main.run_scope.adventure._curio_pending.is_empty() and String(main.curio_overlay._header).contains("关卡奖励点"): + _tested_curio_head = true + var ch := String(main.curio_overlay._header) + if not ch.contains("冒险牌库") or not ch.contains("属性值提升与补给") or ch.contains("同权重") or ch.contains("同上限") or ch.contains("提升池"): + _problems.append("三选一头部旧话残留或新话缺失(wa4——%s)" % ch.replace(" +", "|")) if not main._input_locked(): _problems.append("三选一停点打开时输入未锁定") if main.curio_overlay._continue_btn.visible: @@ -370,7 +433,7 @@ func _ready() -> void: _seals_placed.clear() continue if main.shop_overlay.visible: - # 商店房覆盖层(L2 商店为 Boss 前必经位):零购离开(真实 leave 信号路径)放行选路。 + # 商店房覆盖层(摆放约束:每终点前一位恰一店,真实路径恒必经一店):零购离开(真实 leave 信号路径)放行选路。 # 分派链序:商店层=选路页上的模态覆盖(停点呈现为 # level_screen+shop 双 visible)——必须先于 level_screen 分派,否则下层候选点击 # 空转烧尽预算(实证:lv2 商店停点双 visible 循环 12000 帧 done=false)。 @@ -395,6 +458,18 @@ func _ready() -> void: rbtn2.pressed.emit() if main.status_panel.visible or not main.level_screen.visible: _problems.append("状态面板关闭后地图屏态失守(恢复语义破)") + # 地图屏加成帽换代断言(迭代36 卡A wa2——「特性」→「属性值提升」;帽 Label 常在树; + # 独立旗标——_tested_map_status 在同拍前置块已置位,禁复用其门控) + if not _tested_map_cap: + _tested_map_cap = true + var cap_found := false + var ls_labels: Array = [] + _collect_labels_by_text(main.level_screen, ls_labels) + for l in ls_labels: + if l.text == "属性值提升": + cap_found = true + if not cap_found: + _problems.append("地图屏加成帽未换代为「属性值提升」(wa2)") main.level_screen.press_candidate(_route_clicks % 2) _route_clicks += 1 _seals_placed.clear() @@ -537,6 +612,12 @@ func _ready() -> void: # ① 战败结局清断点(T3 口径——三结局清断点之一):停点快照随局写入,战败结算须清 iso_chk.load_from_disk() if iso_chk.has_breakpoint(): + var bpf: Dictionary = iso_chk.breakpoint_snapshot() + print("[click_probe][diag] 断点未清指纹 keys=%s lv=%s node=%s bonus=%s deck=%s pending=%s" % [ + str((bpf as Dictionary).keys()), str((bpf as Dictionary).get("level_index")), + str((bpf as Dictionary).get("node_id")), str((bpf as Dictionary).get("bonus")), + str(((bpf as Dictionary).get("bonus_deck", []) as Array).size()), + str((bpf as Dictionary).get("bonus_pending"))]) _problems.append("断点段:战败结算后断点未清(T3 三结局清断点失守)") if not main._end_overlay.is_open(): _problems.append("断点段:战败后未出冒险终局面") @@ -553,6 +634,67 @@ func _ready() -> void: # ②③ 中断→续玩全链+互斥确认两分支:随三入口化撤出(卡② §7.2 申报——断点确认探针段移交 # 卡④大门 T5 接收,两头登记禁静默删除;续玩钮已退役,主菜单确认链已摘 mn6)。 # ① 战斗收口断点写盘断言保留(上方——冒险流直调中间态 mn10)。 + + # ---- 开场行三路径段(迭代36 wc1/wc2——真实入口驱动;隔离新档)---- + # 驱动设计(0/0 白板局生存性不成立的实证处置——L2 记录进实现报告): + # ① 菜单开新局·白板反向=独立短局:0/0 滑条开局断言 L1 开场行零后缀(不开驱动—— + # 断言完成后该局被 ② 的开局自然替换,白板局生存性不参与判定); + # ② 局内加算区正向=生存局:400/200 滑条开局保可通关(:363 先例),三选一专选 atk/max_hp + # 卡(无则任选推进——不触后缀、循环重试);至大奖励屏先 reset_sliders() 再确认进下一层 + # ——L2 开场行时点 pct=0 且新档 ov=0,后缀只能归因局内加算区(变异:回退条件 hunk→红); + # ③ 断点续玩正向:L2 入口选路停点(bonus 已入账、bonus_pending/bonus_result 皆空——选路停点形) + # 经 _resume_from_menu 唯一消费口重建,重建开场行带后缀。 + # 判定面=开局清日志后的新局日志(_start_adventure 内 log_view.clear());「(含面板加成)」 + # 唯一来源=开场行,计数即判定;「· 种子 」=开场行独有形态锚。 + for p_ol in ["user://smoke_click_probe_openline.json", "user://smoke_click_probe_openline.json.tmp"]: + if FileAccess.file_exists(p_ol): + DirAccess.remove_absolute(p_ol) + ProbeUiUtil.isolate_save(main, "smoke_click_probe_openline", true, false, true) + main.debug_overlay.reset_sliders() # ① 白板反向前提(_start_adventure 只关窗不清值——残值跨段存活防坑) + main.debug_overlay.seed_edit.text = "42" + main._start_adventure_from_menu() + var ol_l1_wait := 0 + while ol_l1_wait < 24 and String(main.log_view._log.get_parsed_text()).count("· 种子 ") < 1: + ol_l1_wait += 1 + await get_tree().process_frame + if ol_l1_wait >= 24: + _problems.append("开场行路径①:L1 开场行未达日志(开局清日志后 24 帧内未现种子行)") + elif String(main.log_view._log.get_parsed_text()).count("(含面板加成)") > 0: + _problems.append("开场行路径①失守:白板局 L1 开场行带「(含面板加成)」(wc1 反向)") + print("[click_probe] 开场行①判定 wait=%d 帧断言完成" % ol_l1_wait) + # ②③ 断点续玩正向(既有快照喂入形态——代拍板 2 预授权;生产「继续上局」钮已随三入口化 + # 撤出,_resume_from_menu=唯一消费口;断点形态=选路停点形钉值:bonus 已入账、 + # bonus_pending/bonus_result 皆空——set_breakpoint 注入口同 system_test V3i 先例, + # 卡内警示的 bonus_pending 形夹具是本形态反例,禁复用)。续玩重建开场行时点 pct=0 + # +新档 ov=0+bonus.atk=5 已回填(resume_adventure :151-154)——后缀只能归因局内加算区。 + # 【卡片前提缺陷记录(随实现报告/停机呈报)】路径②原设计「奖励节点三选一拿属性卡→下一关 + # 开场行」在现行 build 不可驱动:L1 全模板零商店节点、reward 节点=零兑付直通占位 + # (level_runner _enter_node 注记)——新开局在任一开场行时点均无局内 bonus 来源。 + # 条件加算区两项的机制面由本续玩路径全链承载(回填→level_started→条件求值,与 fresh 同式), + # 变异(回退条件 hunk)见红;fresh 正向拦截能力未证,如实申报。 + var bp_ol: Dictionary = { + "master_seed": 42, "rng_counter": 0, "hero_hp": 30, "gp": 10, "keystone": 0, + "level_index": 1, "bonus": {"atk": 5, "max_hp": 0, "armor_class": 0, "ap": 0}, + "shop_purchased": {}, "relics": [], "keystone_drops": [], "bonus_taken": {}, + "node_id": -1, "visited_ids": [], "graph_gen_v2": 1, + } + main.save_manager.set_breakpoint(bp_ol) + # 判定面=续玩开局清日志后的新局日志(_resume_from_menu 内 log_view.clear()——选路停点形 + # resume 同步重建开场行,返回时已在日志) + main._resume_from_menu() + var ol_r_wait := 0 + while ol_r_wait < 24 and String(main.log_view._log.get_parsed_text()).count("· 种子 ") < 1: + ol_r_wait += 1 + await get_tree().process_frame + if main.run_scope.runner == null or int(main.run_scope.runner.run_ledger.bonus.get("atk", 0)) != 5: + _problems.append("开场行路径②③前提失守:续玩重建后加算区 atk!=5(断点回填链断)") + if ol_r_wait >= 24: + _problems.append("开场行路径②③失守:续玩重建未发开场行(选路停点形应重发——形态/驱动复核)") + elif String(main.log_view._log.get_parsed_text()).count("(含面板加成)") < 1: + _problems.append("开场行路径②③失守:bonus 已入账(pct=0/ov=0)的续玩重建开场行未带「(含面板加成)」(wc1 正向/wc2)") + else: + print("[click_probe] 开场行②③判定:续玩重建开场行带后缀(bonus.atk=5、pct=0)— wait=%d 帧" % ol_r_wait) + print("[click_probe] 循环退出(done=%s budget=%s)问题=%d" % [ str(_done), str(action_budget), _problems.size()]) for p in _problems: @@ -568,6 +710,23 @@ func _ready() -> void: # ---- 模态/tooltip/刷新链一次性覆盖(真实信号路径驱动) ---- func _run_modal_probes_once() -> void: + # ⓪ 玩家面文案真实入口断言(迭代36 卡A wa1——C 区两段头随战斗常驻) + if not _tested_copy and main.info_panel != null and main.info_panel.visible: + _tested_copy = true + var feat_head: Label = null + var curio_head: Label = null + for child in main.info_panel._feat_section.get_children(): + if child is Label: + feat_head = child + break + for child in main.info_panel._curio_section.get_children(): + if child is Label: + curio_head = child + break + if feat_head == null or feat_head.text != "专长": + _problems.append("C 区专长段头未换代(wa1——真实入口)") + if curio_head == null or curio_head.text != "属性值提升": + _problems.append("C 区属性值提升段头未换代(wa1——真实入口)") # ① 图例模态(pattern 6):打开 → 遮挡物隐藏 + 输入锁定;关闭 → 恢复 if not _tested_legend: _tested_legend = true @@ -582,6 +741,10 @@ func _run_modal_probes_once() -> void: for e in main.legend_overlay.ENTRIES: if String(e[1]) == "封印": legend_has_seal = true + # 图例封印行换代断言(迭代36 卡A wa5——真实 AP 条图例钮开层路径) + if not String(e[2]).contains("标空") or not String(e[2]).contains("受挖掘惩罚") or String(e[2]).contains("插错") or String(e[2]).contains("挖雷"): + _problems.append("图例封印行旧词残留(wa5——%s)" % String(e[2]).replace(" +", "|")) if not legend_has_seal: _problems.append("图例缺『封印』格态条目(格态图例基线)") main.legend_overlay.close() @@ -605,6 +768,46 @@ func _run_modal_probes_once() -> void: var bal_txt: String = main.status_panel._balance_badge.value_label.text if not bal_txt.begins_with("钥石 "): _problems.append("状态面板余额徽投影缺失(%s)" % bal_txt) + # ---- 本局信息面板换代断言(迭代36 卡③——真实入口三断言)---- + # ① 面板子树零旧段文案(跨关概览退役零残留) + var sv_labels: Array = [] + _collect_labels_by_text(main.status_panel, sv_labels) + for l in sv_labels: + if String(l.text).contains("道具工坊") or String(l.text).contains("天赋树"): + _problems.append("状态面板子树残留旧段文案「%s」(sv6①——零旧段失守)" % String(l.text)) + # ② 属性行全值 = 契约4 格式 % runner 五现值(main 侧投影值层真链路) + var rnr = main.run_scope.runner + if rnr == null: + _problems.append("状态面板开层时 runner 空指针(契约5——状态机缺陷,禁软回退)") + else: + var attr_expect := "生命 %d/%d · 攻击 %d · 护甲 AC %d · GP %d" % [ + int(rnr.hero_hp), rnr.effective_max_hp(), rnr.effective_atk(), + rnr.effective_ac(), int(rnr.gp)] + if String(main.status_panel._attrs_label.text) != attr_expect: + _problems.append("状态面板属性行投影失真(sv6②——期望 %s 实测 %s)" % [attr_expect, main.status_panel._attrs_label.text]) + # ③ _status_data() 直调对拍(sv7——键集恰八键+curios/feats 独立 oracle) + var sd: Dictionary = main._status_data() + var keys := ["balance", "hero_hp", "max_hp", "atk", "ac", "gp", "curios", "feats"] + var keys_ok: bool = (sd as Dictionary).size() == 8 + for k in keys: + keys_ok = keys_ok and (sd as Dictionary).has(k) + if not keys_ok: + _problems.append("_status_data() 键集≠八键(sv7——实测 %s)" % str((sd as Dictionary).keys())) + var oracle: Array = [] + for rec in rnr.treasure_inventory.query_inventory(Callable()): + var rr: Dictionary = rec + oracle.append({"name": String(main.db.treasure_item_row(String(rr.get("item_id", ""))).get("display_name", "")), + "used": bool(rr.get("used", false))}) + var sd_curios: Array = (sd as Dictionary).get("curios", []) + var curios_ok: bool = (sd_curios as Array).size() == (oracle as Array).size() + if curios_ok: + for i in (oracle as Array).size(): + curios_ok = curios_ok and String((sd_curios[i] as Dictionary).get("name", "")) == String((oracle[i] as Dictionary).get("name", "")) and bool((sd_curios[i] as Dictionary).get("used", false)) == bool((oracle[i] as Dictionary).get("used", false)) + if not curios_ok: + _problems.append("_status_data().curios 对拍独立 oracle 失守(sv7——实测 %s vs %s)" % [str(sd_curios), str(oracle)]) + var feats_expect: Array = load("res://presentation/hud_presenter.gd").feat_holdings_lines(main.db, rnr.feats_held()) + if str((sd as Dictionary).get("feats", [])) != str(feats_expect): + _problems.append("_status_data().feats 对拍 feat_holdings_lines 失守(sv7)") var resume_btn: Button = ProbeUiUtil.find_button(main.status_panel, "继续", "prefix") if resume_btn == null: _problems.append("状态面板关闭钮缺失(「继续」)") @@ -660,6 +863,23 @@ func _run_modal_probes_once() -> void: _problems.append("HUD 刷新余量与快照推导失符(HUD='%s' 应含 %d)" % [main.ap_bar.refresh_remaining_text(), remain_expect]) +# ---- 营地灰字取色断言辅助(迭代36 wc4——文本锚定位,全 Label 递归收集)---- + +func _collect_labels_by_text(root: Node, out: Array) -> void: + if root is Label: + out.append(root) + for child in root.get_children(): + _collect_labels_by_text(child, out) + + +func _labels_with_text(labels: Array, needle: String) -> Array: + var found: Array = [] + for l in labels: + if String(l.text).contains(needle): + found.append(l) + return found + + func _play_one_action() -> void: var settlement = main.run_scope.settlement if settlement == null: diff --git a/game/probe/ui_contrast_probe.gd b/game/probe/ui_contrast_probe.gd index c2cf240c..bfe6403c 100644 --- a/game/probe/ui_contrast_probe.gd +++ b/game/probe/ui_contrast_probe.gd @@ -58,10 +58,22 @@ func _pairs() -> Array: ["设置·选项标签/设置板", UiKit.INK_LABEL, UiKit.PANEL_DATA, 1.0], # 设置行标签/状态字(标签深墨) ["三选一·停点头部墨/暗页底", UiKit.WARM_SOFT, UiKit.BG_ROOT, 1.0], # 三选一停点头部/回血反馈行(page_shell 根底档) ["三选一·卡名墨/货架板", UiKit.INK_TITLE, UiKit.PANEL_DATA, 1.0], # bcard 选项卡名(与商店货架同构卡) - ["迷雾·记忆墨/灰罩合成底", UiKit.FOG_MEMORY_INK, # fog_state2 记忆板 α.66 压 floor avg #453020 - Color(UiKit.FOG_MEMORY_PLATE.r * 0.66 + 69.0 / 255.0 * 0.34, # 合成底(素材侧值无常量形态——取 art-bible §389 在册复算值,素材更新随动; - UiKit.FOG_MEMORY_PLATE.g * 0.66 + 48.0 / 255.0 * 0.34, # 三关逐关 avg 缺在册且素材不入 git 不可实查——最坏情况判据扩对归移交) - UiKit.FOG_MEMORY_PLATE.b * 0.66 + 32.0 / 255.0 * 0.34), 1.0], + # ---- 迷雾测点三关扩对(迭代36 wc5——甲·归一后基准:行值=三关归一后(真实渲染面) + # 地板均值合成底(写卡会话 PIL 复算+两轮 grilling 独立复算吻合;T0 探针派生首跑对拍 + # L2/L3 修值(PIL↔Godot 导入解码系统差约 5/255,卡内 L2 条款预授权以派生为准); + # 素材更新随动——一致性由 _ready 派生校验断言钉住,行值回原图基准或素材换代未随动均红)---- + ["迷雾·记忆墨/合成底L1地牢", UiKit.FOG_MEMORY_INK, + Color(UiKit.FOG_MEMORY_PLATE.r * 0.66 + 111.0 / 255.0 * 0.34, # L1 归一后地板均值 #6F4D33(合成底 #4C4038 5.71:1·最差关) + UiKit.FOG_MEMORY_PLATE.g * 0.66 + 77.0 / 255.0 * 0.34, + UiKit.FOG_MEMORY_PLATE.b * 0.66 + 51.0 / 255.0 * 0.34), 1.0], + ["迷雾·记忆墨/合成底L2幽暗", UiKit.FOG_MEMORY_INK, + Color(UiKit.FOG_MEMORY_PLATE.r * 0.66 + 74.0 / 255.0 * 0.34, # L2 归一后地板均值 #4A4F60(合成底 #3F4147 5.83:1) + UiKit.FOG_MEMORY_PLATE.g * 0.66 + 79.0 / 255.0 * 0.34, + UiKit.FOG_MEMORY_PLATE.b * 0.66 + 96.0 / 255.0 * 0.34), 1.0], + ["迷雾·记忆墨/合成底L3深幽", UiKit.FOG_MEMORY_INK, + Color(UiKit.FOG_MEMORY_PLATE.r * 0.66 + 71.0 / 255.0 * 0.34, # L3 归一后地板均值 #474B63(合成底 #3E4048 5.93:1) + UiKit.FOG_MEMORY_PLATE.g * 0.66 + 75.0 / 255.0 * 0.34, + UiKit.FOG_MEMORY_PLATE.b * 0.66 + 99.0 / 255.0 * 0.34), 1.0], ] @@ -94,8 +106,9 @@ func _ready() -> void: if r < MIN_RATIO: problems.append("[contrast] %s = %.2f:1 < %.1f(测点对须垫牌或换墨)" % [p[0], r, MIN_RATIO]) problems.append_array(_scan_static()) + problems.append_array(_fog_derived_check()) if problems.is_empty(): - print("[ui_contrast_probe] PASS — 对比度 %d 对测点全绿(≥%.1f:1 无区域例外)+ 裸色白名单/preload 红线静态段绿" % [pairs.size(), MIN_RATIO]) + print("[ui_contrast_probe] PASS — 对比度 %d 对测点全绿(≥%.1f:1 无区域例外)+ 裸色白名单/preload 红线静态段绿 + 迷雾三关派生校验绿" % [pairs.size(), MIN_RATIO]) get_tree().quit(0) else: for msg in problems: @@ -104,6 +117,52 @@ func _ready() -> void: get_tree().quit(1) +# ---- 迷雾测点行值一致性派生校验(迭代36 wc5——行值 vs 运行时按生产归一函数实算的 +# 三关归一后地板均值,容差 ±2/255=8-bit 量化级;行值回原图基准或素材换代未随动均红)---- +func _fog_derived_check() -> Array: + var problems: Array = [] + var rows := [ + ["L1地牢", "dungeon", Color8(111, 77, 51)], + ["L2幽暗", "underdark", Color8(74, 79, 96)], + ["L3深幽", "deep_underdark", Color8(71, 75, 99)], + ] + for row in rows: + var derived := _norm_floor_avg(row[1]) + var expect: Color = row[2] + if absi(derived.r8 - expect.r8) > 2 or absi(derived.g8 - expect.g8) > 2 or absi(derived.b8 - expect.b8) > 2: + problems.append("[contrast] 迷雾测点行值失真:%s 行值 (%d,%d,%d) vs 运行时归一派生 (%d,%d,%d)(±2/255——素材更新须随动改行值)" + % [row[0], expect.r8, expect.g8, expect.b8, derived.r8, derived.g8, derived.b8]) + return problems + + +# 迷雾测点派生助手(迭代36 wc5 契约 3——全仓唯一「按板型算归一后均值」判据面):纹理路径经 +# BoardView.TILE_FLOOR_FMT 常量(禁复制字面量)、归一经 BoardView._normalize_floor_luma +# 生产静态函数直呼(真渲染同源——禁探针内复制 k 公式);逐像素归一后均值再五变体平均。 +static func _norm_floor_avg(board_type: String) -> Color: + var sum_r := 0.0 + var sum_g := 0.0 + var sum_b := 0.0 + var px := 0 + for i in range(1, BoardView.TILE_FLOOR_VARIANTS + 1): + var tex := UiKit.load_tex(BoardView.TILE_FLOOR_FMT % [board_type, i]) + if tex == null: + continue + var img: Image = BoardView._normalize_floor_luma(tex, 0.10, null).get_image() + if img.is_compressed(): + img.decompress() + img.convert(Image.FORMAT_RGBA8) + for y in range(img.get_height()): + for x in range(img.get_width()): + var c := img.get_pixel(x, y) + sum_r += c.r + sum_g += c.g + sum_b += c.b + px += 1 + if px == 0: + return Color(-1, -1, -1) # 素材缺位=派生校验必红(禁跳过——契约 3 齐盘条款) + return Color(sum_r / px, sum_g / px, sum_b / px) + + # ---- 机检段②③(需求卡片 §7):呈现层裸 Color8( 计数 vs 白名单 + 素材 preload 计数 = 0 ---- func _scan_static() -> Array: var problems: Array = [] diff --git a/game/probe/ui_struct_probe.gd b/game/probe/ui_struct_probe.gd index 93141b51..2c7bc0f3 100644 --- a/game/probe/ui_struct_probe.gd +++ b/game/probe/ui_struct_probe.gd @@ -43,6 +43,11 @@ func _ready() -> void: else: _settle() await _audit("camp", [main.camp_overlay]) + # 营地灰字源 token 扫描(迭代36 wc4 静态半——源内零裸 UiKit.STONE;判别式= + # STONE 出现次数 == STONE_DEEP 出现次数,即每处 STONE 都带 _DEEP 后缀) + var camp_src := FileAccess.get_file_as_string("res://presentation/views/camp_overlay_view.gd") + if camp_src.count("UiKit.STONE") != camp_src.count("UiKit.STONE_DEEP"): + problems.append("[camp] camp_overlay_view.gd 存在裸 UiKit.STONE(非 _DEEP 后缀——wc4 源扫描)") main._on_camp_closed() _settle() main._on_save_select_closed() # 关闭被栈还原的选档层(动线序中间态——卡④ 换代点) @@ -252,11 +257,12 @@ func _ready() -> void: get_tree().quit(1) -# ⑦resume 夹具:最小合法断点快照——bonus_pending 非空=三选一停点分支(合计 gp=24 供守卫兜底断言) +# ⑦resume 夹具:最小合法断点快照(含生成图代差标记键)——bonus_pending 非空=三选一停点分支(合计 gp=24 供守卫兜底断言) func _resume_bp_fixture() -> Dictionary: return { "master_seed": 42, "rng_counter": 0, "hero_hp": 20, "gp": 24, "keystone": 0, "level_index": 1, "bonus": {}, "shop_purchased": {}, "relics": [], + "graph_gen_v2": 1, # 生成图换代代差标记键(缺键=换代前旧断点,resume_adventure 显式拒绝) "keystone_drops": [], "bonus_taken": {}, "bonus_pending": {"candidates": [{"kind": "heal", "tier": "common"}], "payload": {"gp_dropped": 0}}, } diff --git a/game/sim/adventure_monte_carlo.gd b/game/sim/adventure_monte_carlo.gd index 95cbbae3..5c02fb9d 100644 --- a/game/sim/adventure_monte_carlo.gd +++ b/game/sim/adventure_monte_carlo.gd @@ -136,7 +136,7 @@ func _initialize() -> void: "encounter_max_hp_seq": st["encounter_max_hp_seq"], "reward_events": st["reward_events"], "shop_gp_seq": st["shop_gp_seq"], - "level_templates": st.get("level_templates", []), + "level_maps": st.get("level_maps", []), "wallet_count": (st["wallet_events"] as Array).size(), } if shop_mode == "on": diff --git a/game/sim/level_autoplay.gd b/game/sim/level_autoplay.gd index 7c6a13dc..a719f0d1 100644 --- a/game/sim/level_autoplay.gd +++ b/game/sim/level_autoplay.gd @@ -9,7 +9,7 @@ # 策略档 policy:"normal"(默认)/ "perfect"(零盲挖/全封印/卡死刷新——上界测量仪,求解器与 # 动作语义见 auto_player.gd 完美策略档节)。 # 选路驱动(零自动推进):兑付确认/战斗胜利后 runner 进选路态,驱动循环按 route 策略显式 -# pick_next_node;route = "min"/"max"(候选节点编号最小/最大——确定性路径轴,零随机流自开;默认 "min")。 +# pick_next_node;route = "min"/"max"(候选节点句柄最小/最大——确定性路径轴,零随机流自开;默认 "min")。 # run_adventure 三关连探驱动(AdventureRunner——主种子恰一次、通关兑付停点确认、adventure_finished # 置 result/summary;外层每关重取 runner 实例经 _drive_level 复用单关驱动)。 # 大奖励停点 = 三选一驱动(候选取首件/替换放弃新件)。 @@ -34,7 +34,7 @@ const NORMAL_SKIP_DIG_P := 0.05 const NORMAL_IDLE_GAMBLE_P := 0.10 -static func run_level(db, seed_val: int, level_index: int, atk_pct: int, hp_pct: int, collect_rows: bool = false, logger = null, policy: String = "normal", route: String = "min", template_pick: int = -1) -> Dictionary: +static func run_level(db, seed_val: int, level_index: int, atk_pct: int, hp_pct: int, collect_rows: bool = false, logger = null, policy: String = "normal", route: String = "min") -> Dictionary: var own_session := logger == null if own_session: logger = load("res://presentation/playtest_logger.gd").new() @@ -60,7 +60,7 @@ static func run_level(db, seed_val: int, level_index: int, atk_pct: int, hp_pct: runner.node_entered.connect(_on_node_entered.bind(runner, st, logger)) runner.level_finished.connect(_on_level_finished.bind(st, logger)) runner.set_hero_override(atk_pct, hp_pct) - if not runner.start_level(level_index, seed_val, {}, template_pick): + if not runner.start_level(level_index, seed_val, {}): st.result = "start_failed" runner.restore_hero_base() if own_session: @@ -138,7 +138,7 @@ static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_ "prng": _policy_rng(seed_val), "mine_slips": 0, "skip_digs": 0, "shop_buys": 0, "adv": true, # v3 档位行为开关(选路评分/失误注入/商店购买只挂冒险测量面——单关夹具语义不变) - "level_templates": [], # 逐关图模板名(关卡节点图生成 v0.4 分层出数) + "level_maps": [], # 逐关节点图标识(生成图 = 关卡序号+本局主种子——MC 分层出数) } adr.step_row.connect(_on_step_row.bind(adr, st, collect_rows)) adr.node_entered.connect(_on_adventure_node_entered.bind(adr, st, logger)) @@ -150,7 +150,7 @@ static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_ adr.adventure_finished.connect(_on_adventure_finished.bind(st, logger)) adr.set_hero_override(atk_pct, hp_pct) # 同调分层注入(缺省 {} 与既有调用方逐位一致零行为变更) - var adv_params := {"hero_override": hero_override, "first_script": false} + var adv_params := {"hero_override": hero_override} if not attune.is_empty(): adv_params["attunement"] = attune.get("equipped", []) adv_params["attune_refresh_y"] = int(attune.get("refresh_y", 0)) @@ -197,9 +197,9 @@ static func _on_adventure_node_entered(node: Dictionary, info: Dictionary, adr, var r = adr.runner() var t := String(node.get("type", "")) st.visited_ids.append(int(r.node_id_of(node))) - if not st.has("level_templates"): - st["level_templates"] = [] - st["level_templates"].append(r.active_template_name) # MC 分层出数(关卡×图模板——v0.4 测量对账条款) + if not st.has("level_maps"): + st["level_maps"] = [] + st["level_maps"].append("%d#%d" % [int(r.level_row.get("level_index", 0)), int(adr.master_seed)]) # MC 分层出数(关卡×生成图标识) if t == "battle" or t == "boss": st.battles += 1 @@ -208,7 +208,7 @@ static func _on_adventure_node_entered(node: Dictionary, info: Dictionary, adr, if logger != null and r.current_battle() != null: logger.attach_battle(r.current_battle()) if t == "boss": - st.boss_depth = int(node.get("node_depth", -1)) + st.boss_depth = int(node.get("depth", -1)) st.hp_at_boss = int(adr.ledger["hero_hp"]) elif t == "reward": # 奖励节点三选一(共享池确定性策略;TP0 起 316-f 判读:1 级最稀有、 @@ -376,7 +376,7 @@ static func _policy_rng(seed_val: int) -> RandomNumberGenerator: # 路径轴选路(目标价值评分制):候选按节点类型评分取最高, -# 同分 tie-break 沿 route 方向(min=小编号/max=大编号——verifier 双窗轴保持)。 +# 同分 tie-break 沿 route 方向(min=小句柄/max=大句柄——verifier 双窗轴保持)。 # 评分语义(确定性,零随机流):rest=伤重高值(缺口比例×100)/shop=买得起高值(GP 局内死钱, # 真人实证两店全购)/reward=占位低值(MC 夹具解锁空集=零兑付直通)/boss=缓冲低值(有店有_rest # 先补给再打)/battle=基线。编号取 runner.next_candidate_ids()(编号空间解析单一出口)。 @@ -478,7 +478,7 @@ static func _on_node_entered(node: Dictionary, info: Dictionary, runner, st: Dic if logger != null and runner.current_battle() != null: logger.attach_battle(runner.current_battle()) if t == "boss": - st.boss_depth = int(node.get("node_depth", -1)) + st.boss_depth = int(node.get("depth", -1)) st.hp_at_boss = runner.hero_hp elif t == "reward": runner.continue_after_node() # 奖励点 = 内容未定直通节点 diff --git a/game/tests/README.md b/game/tests/README.md index f400bc65..8a376419 100644 --- a/game/tests/README.md +++ b/game/tests/README.md @@ -1,4 +1,4 @@ -# Test Infrastructure — Dungeon Sweeper +# Test Manual — Dungeon Sweeper(测试工具手册) **Engine**: Godot 4.7.2-stable(GDScript 单语言) **Test Framework**: 标准四级测试层(冒烟/单元/集成/系统)+ headless 场景探针与渲染 golden;gdUnit4 **deferred**(D2 OQ1 关闭形态——本仓库不引第三方测试框架,需求见下「门禁」节) @@ -6,6 +6,11 @@ **Setup date**: 2026-09-12(2026-09-27 `7f24b640` 三层收敛:根 tests/ 并入 `game/tests/`、docs 并入 `design/engineering/`、production 解散) **参考实现**: `game/probe/`(场景探针族)· `game/sim/`(测量/诊断/模拟驱动件)(场景探针族——正式工程的活样板;系统测试断言面过渡宿主 `smoke_all.gd` 亦在此,迁移归断言迁移批) +本手册是测试面每件工具的介绍处:**它是什么、判什么、怎么单独跑、结果由谁判、什么时候会用它**。 +分工三线——介绍归本文;**规则与时机**(什么时候必须跑哪些测试、测试怎么写、怎么算合格)归 +`design/engineering/test-conventions.md`;**收口验收跑哪几道闸**(闸清单与改动域划分)只在 +`tools/run_all_gates.sh` 脚本内定义一处,本手册不另列闸清单。 + ## Directory Layout ``` @@ -47,6 +52,21 @@ tests/ 两口径数值不同且各自浮动,禁互相换算或写死。 +## 工具速查(逐件:判什么 / 结果谁判 / 什么时候用) + +| 工具 | 判什么 | 结果谁判 | 什么时候会用它 | +|---|---|---|---| +| `tools/run_smoke.py`(冒烟测试) | 这版能否启动、能否加载表、能否进主屏——零逻辑判定 | 退出码+脚本错误扫描 | 开机路径/表加载改动后的快速自检;收口由闸编排 | +| `tools/run_system_test.py`(系统测试) | 真实玩法路径的行为断言面(推理→战斗→结算→进度推进) | 退出码+脚本错误扫描 | 玩法链路改动的调试期确认 | +| `tests/run_all.gd`(单元与集成) | 公式/状态机/契约/跨系统闭环(40 套件 manifest) | 退出码+三硬闸(见下) | 新套件单跑调试;逻辑改动的最小单元复跑 | +| `tools/probe_gate.sh`(场景探针,`--click`/`--full` 两模式) | UI 可达性、可见性、状态——真实实例化场景+真实输入路径 | 探针自检+退出码 | 新增 UI 区域的配套探针直跑与断言复核 | +| `tools/render_gate.sh`(渲染 golden) | 画面基线——截图与基线图逐像素容差比对 | golden 比对脚本(依赖 PIL) | 主题/素材/排版类改动;窗口化运行,需开显示器 | +| `game/tests/static_scan.sh`(S 域) | 项目纪律+第三方质量规则:RNG 单点/core 纯度/类型标注/文件头/注释禁词/旧符号残留;S13=gdstyle 质量规则(重复 load、循环内 await、循环内分配、不可达代码等真缺陷类,存量基线外新违规才红) | 退出码(逐规则 FAIL 行) | 改产品码前后的快速自检;新增纪律先补扫描规则再修码 | +| `tools/parity_anchor.py`(回放对拍) | 同种子下三变体逐位确定性 | 工具脚本逐位比对 | 本批触及数值/存档/RNG/结算面时;须用户批准发起 | +| 测量件(`game/sim/`,逐件见下「游离脚本台账」) | 期望/分布读数——只给读数,不判对错 | 人工读 stdout 或配套工具脚本 | 调参与归因;MC 类一律用户发起(档位表见 `sim-data/README.md`「MC 采集档位表」) | + +> 单件直跑是调试期最窄确认手段,**不构成验收**——验收只有 `tools/run_all_gates.sh` 一个入口(判定三硬闸见下「本地闸」节)。 + ## Running Tests ```bash @@ -121,6 +141,7 @@ timeout 120s godot --headless --script tests/run_all.gd > **纪律**:本表是这些件的唯一在册处。新增手动测量脚本必须同批登记(用途 + 触发方式 + 结果由谁判); > 退役走 `design/engineering/doc-conventions.md` 第 10 节退役规则,禁止「没看见=死件」式清理。 +> 测量类批量的发起规则(档位与批准)见 `sim-data/README.md`「MC 采集档位表」——无数值改动禁重跑。 ## Static Scan(提交前本地跑) diff --git a/game/tests/integration/cross_system_flow_test.gd b/game/tests/integration/cross_system_flow_test.gd index 8543e0ab..8d3a68a5 100644 --- a/game/tests/integration/cross_system_flow_test.gd +++ b/game/tests/integration/cross_system_flow_test.gd @@ -76,7 +76,7 @@ func _feats_breakpoint_roundtrip() -> void: var live_rows: FeatTable = _db.feats _db.feats = rt var a_r = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(72)) - a_r.start_adventure(72, {"first_script": true}) # 源 :723(受理断言留单元宿主) + a_r.start_adventure(72) # 源 :723(受理断言留单元宿主) a_r._curio_pending = {"candidates": [{"kind": "relic", "tier": "rare", "feat_id": "x3"}, {"kind": "gp", "tier": "common"}], "payload": {}} a_r.pick_curio("relic", "rare", "x3") # 源 :781(兑付断言留单元宿主) # 断点往返(持有集+余库 feat_id 随行) diff --git a/game/tests/integration/node_map_templates_test.gd b/game/tests/integration/node_map_templates_test.gd index 839fc9c0..4a36e01d 100644 --- a/game/tests/integration/node_map_templates_test.gd +++ b/game/tests/integration/node_map_templates_test.gd @@ -1,18 +1,21 @@ -# tests/integration/node_map_templates_test.gd — L2 集成用例:关卡节点图生成·模板池 -# 断言面 = 模板池套(批 B 断言迁移自 smoke_all.gd `_smoke_node_map_templates`,G12 施工组; +# tests/integration/node_map_templates_test.gd — L2 集成用例:关卡节点图生成·确定性承载 +# 断言面 = 生成图确定性套(批 B 断言迁移自 smoke_all.gd,G12 施工组; # 主会话裁决六整套集成不拆——跨四系统且走落盘)。 -# 跨系统面 = RngService(node_map 用途标签/抽签确定性/零消耗计账)× -# LevelRunner(start_level_external 受理/模板 0 回滚档)× -# AdventureRunner(first_script 全关零抽签/逐关恰一抽签)× +# 跨系统面 = RngService(纯派生流零计数器占用)× +# LevelRunner(start_level/start_level_external 受理·生成图装配·单一图源)× +# AdventureRunner(开局种子承载·断点写读后同图)× # SaveManager+MetaProgression(首次冒险布尔位收束置位落盘——真实档写盘)。 # 与单元侧同入口同协议:extends SceneTree+Kit.check 主通道;--script 模式无 autoload, -# ConfigDB 显式实例化构造注入;表加载失败不中止(沿被迁断言面的源语义——诊断展开,红由断言落)。 +# ConfigDB 显式实例化构造注入;表加载失败不中止(沿被迁断言面的源语义——诊断展开,红由具体断言落)。 # 运行:godot --headless --path game --script res://tests/integration/node_map_templates_test.gd extends SceneTree const Kit := preload("res://tests/unit/test_kit.gd") +const SaveManagerScript := preload("res://core/save_manager.gd") +const AdventureRunnerScript := preload("res://core/adventure_runner.gd") var _db +var _bp_path := "user://smoke_node_map_save.json" func _init() -> void: @@ -24,84 +27,121 @@ func _init() -> void: for d in _db.diagnostics(): if d.severity == "error" or d.severity == "fatal": push_error(" [%s] %s: %s" % [d.severity, d.category, d.message]) - _smoke_node_map_templates() + _smoke_generated_map_determinism() quit(Kit.finish("node_map_templates_test")) -# ---- 关卡节点图生成·模板池冒烟(v0.4 §P0.6 断言面)---- -func _smoke_node_map_templates() -> void: - # ① 扩签:node_map 入用途标签封闭词表(派生零改动——标签只进事件日志) - var svc = load("res://core/rng_service.gd").new() - Kit.check("node_map 标签入 rng_service 封闭词表(扩签通道=改代码+冒烟守卫)", - (svc.PURPOSE_TAGS as Array).has("node_map")) - # ② 池参数:三关各 5 模板、模板 0 存在(回滚档成员) - for li in range(1, 4): - var row := {} - for r in _db.level.rows: - if int(r.level_index) == li: - row = r - var tpls: Array = row.get("templates", []) - Kit.check("L%d 模板池 = 5 张(拍板点④——首版池规模)" % li, tpls.size() == 5) - Kit.check("L%d 模板 0 = 现行固定图(回滚档成员)" % li, - not tpls.is_empty() and String(tpls[0].get("display_name", "")).ends_with("(现行)")) - # ③ 强制取 0 = 模板 0 节点表逐位相等 + 零 scope 消费 - var rng0 = load("res://core/rng_service.gd").new() - rng0.new_adventure(910001) - var runner0 = load("res://core/level_runner.gd").new(_db, rng0) - var c_before: int = rng0.counter() - Kit.check("强制 template_pick=0 起关受理", runner0.start_level_external(2, {"hero_hp": 20, "gp": 0}, {}, # start_level_external 签名收口(bonus 参数由 RunLedger 单类下传) - Callable(self, "_smoke_rest_stub"), {}, true, 0)) - Kit.check("强制取模板零 RNG 消耗(零抽签——首次脚本语义)", rng0.counter() == c_before) - var tpl0: Dictionary = {} - for r in _db.level.rows: - if int(r.level_index) == 2: - tpl0 = r.get("templates", [])[0] - Kit.check("强制 0 的生效节点表 = 模板 0 逐位相等(回滚档成立)", - runner0.level_row.get("nodes", []) == tpl0.get("nodes", [])) - # ④ 抽签确定性:同种子两实例同模板(同种子同操作序列 → 逐位同图) - var names := [] - for i in range(2): - var rg = load("res://core/rng_service.gd").new() - rg.new_adventure(910002) - var rn = load("res://core/level_runner.gd").new(_db, rg) - rn.start_level_external(2, {"hero_hp": 20, "gp": 0}, {}, - Callable(self, "_smoke_rest_stub"), {}, true, -1) - names.append(rn.active_template_name) - Kit.check("抽签确定性:同种子逐位同图(实测 %s)" % String(names[0]), - names[0] == names[1] and String(names[0]) != "") - # ⑤ N=1 池与现行固定图等价:单模板行过图校验(抽签取模 1 恒 0 = 回滚档) - var lt = load("res://data/schemas/level_table.gd").new() - lt.rows.assign([{"level_index": 9, "display_name": "t", "rest_heal_pct": 30, - "templates": [tpl0]}]) - Kit.check("N=1 池表通过图校验(回滚档形态合法)", lt.validate().is_empty()) - # ⑥ 首次脚本分支:first_script=true 全关模板 0;false = 抽签(scope 恰一差异) - var rngF = load("res://core/rng_service.gd").new() - rngF.new_adventure(910003) - var adrF = load("res://core/adventure_runner.gd").new(_db, rngF) - adrF.start_adventure(910003, {"first_script": true}) - Kit.check("首次脚本模式:L1 取模板 0(全关零抽签——存档布尔位 false 语义)", - String(adrF.runner().active_template_name).ends_with("(现行)")) - var cF: int = rngF.counter() - var rngT = load("res://core/rng_service.gd").new() - rngT.new_adventure(910003) - var adrT = load("res://core/adventure_runner.gd").new(_db, rngT) - adrT.start_adventure(910003, {"first_script": false}) - Kit.check("抽签模式恰多消费 1 scope(node_map 逐关恰一——L1 起关抽签)", rngT.counter() == cF + 1) - Kit.check("抽签模式 active_template_name 有值(MC 分层出数取数面在位)", - String(adrT.runner().active_template_name) != "") +# ---- 关卡节点图生成·确定性承载断言面 ---- +func _smoke_generated_map_determinism() -> void: + # ① 纯函数复现:图 = f(参数档, 种子)——同档同种子两次产图逐位同图,换种子必变图 + var gen = NodeMapGenerator.new(_fresh_rng(910001)) + var params2: Dictionary = _db.level_gen_params_row(2) + var g_a: Dictionary = gen.generate(params2, 910002) + var g_b: Dictionary = gen.generate(params2, 910002) + var g_c: Dictionary = gen.generate(params2, 910003) + var nodes_a: Array = g_a.get("nodes", []) + Kit.check("同参数档同种子两次产图逐位同图(D2 执行边界)〔v7〕", + nodes_a == (g_b.get("nodes", []) as Array)) + Kit.check("同参数档换种子即变图(图确为种子函数,非恒定输出)", + nodes_a != (g_c.get("nodes", []) as Array)) + Kit.check("产图节点携图内唯一句柄 id 与自入口 depth(消费侧寻址面)", + _not_empty_graph(nodes_a) and _handles_unique(nodes_a)) + # ② 消费侧单一图源:起关装配的图与生成器直取逐位一致(runner 内不存第二份图数据) + var runner1 = load("res://core/level_runner.gd").new(_db, _fresh_rng(910004)) + Kit.check("单关起关受理(种子 910004——生成图直供,无兜底图)", + runner1.start_level(2, 910004)) + Kit.check("起关生效图 = 生成器同档同种子直取逐位一致(消费侧无第二图源)〔v7〕", + runner1.level_row.get("nodes", []) == (gen.generate(params2, 910004) as Dictionary).get("nodes", [])) + # ③ 产图零统一流消费(纯派生流不占计数器号位——断点在任意号位重生成同图的机制承载) + var rng_c = _fresh_rng(910005) + var counter_before: int = rng_c.counter() + var runner2 = load("res://core/level_runner.gd").new(_db, rng_c) + Kit.check("冒险模式起关受理(入口停点)", runner2.start_level_external(3, + {"hero_hp": 20, "gp": 0}, {}, Callable(self, "_smoke_rest_stub"), {}, true)) + Kit.check("起关产图零计数器消费(同图不依赖取数号位)", rng_c.counter() == counter_before) + var params3: Dictionary = _db.level_gen_params_row(3) + var entry_count: int = int(params3.get("entry_count", -1)) + Kit.check("入口停点候选数=参数档入口个数(L3 档 %d——多入口首跳即选路,禁写死单入口)〔v2〕" % entry_count, + entry_count >= 1 and (runner2.next_candidates() as Array).size() == entry_count) + if entry_count >= 2: + var eids: Array = runner2.next_candidate_ids() + Kit.check("第二入口经句柄显式进入(句柄 %d)〔v2〕" % int(eids[1]), + runner2.pick_next_node(int(eids[1])) and int(runner2.current_node_id()) == int(eids[1])) + # ④ 开局种子承载:新档(首局)种子 42 两次开局逐位同图,且与生成器直取一致 + DirAccess.remove_absolute(_bp_path) # 测试隔离——上轮落盘残留先删,断点面从零档起 + var sv_w = SaveManagerScript.new(_bp_path) + var adr_x = AdventureRunnerScript.new(_db, _fresh_rng(42)) + adr_x.set_save_manager(sv_w) # 先挂档再开局——停点快照由真实写手落盘 + Kit.check("首局口径种子 42 开局受理(defer_entry 路径)", adr_x.start_adventure(42, + {"hero_override": {}, "treasure_unlocked": {}})) + var adr_y = AdventureRunnerScript.new(_db, _fresh_rng(42)) + Kit.check("同种子第二次开局受理", adr_y.start_adventure(42, + {"hero_override": {}, "treasure_unlocked": {}})) + Kit.check("种子 42 两次开局图逐位同图(教学局恒同图——首局固定种子的确定性承载)〔v7〕", + adr_x.runner().nodes() == adr_y.runner().nodes()) + Kit.check("开局图 = L1 参数档在种子 42 下的生成图(开局无隐式取数分叉)", + adr_x.runner().nodes() == (gen.generate(_db.level_gen_params_row(1), 42) as Dictionary).get("nodes", [])) + # ⑤ 断点写→恢复→再产图同图(续玩按主种子+参数档重生成,断点不存图快照) + Kit.check("停点断点落盘在档", sv_w.has_breakpoint()) + var bp: Dictionary = sv_w.breakpoint_snapshot() + var adr_z = AdventureRunnerScript.new(_db, load("res://core/rng_service.gd").new()) + Kit.check("断点续玩受理(代差标记键在档)〔v8〕", adr_z.resume_adventure(bp, + {"hero_override": {}, "treasure_unlocked": {}})) + Kit.check("续玩重生成图与中断前逐位同图〔v7〕", adr_z.runner().nodes() == adr_x.runner().nodes()) + var adr_old = AdventureRunnerScript.new(_db, load("res://core/rng_service.gd").new()) + var bp_old: Dictionary = bp.duplicate(true) + bp_old.erase("graph_gen_v2") # 换代前旧断点形态(模板池时代无代差标记键) + Kit.check("缺生成图代差标记键的旧断点被显式拒绝(禁按新语义静默回放)〔v8〕", + adr_old.resume_adventure(bp_old, {}) == false) + # ⑥ 关卡表不再携带图数据(图源换代后 level 行只剩行级字段) + var tpl_free := true + for li in range(1, _db.level_count() + 1): + if (_db.level_row(li) as Dictionary).has("templates"): + tpl_free = false + Kit.check("level 表三行均无 templates 键(模板数据已清出)〔v9〕", + tpl_free and _db.level_count() == 3) # ⑦ 收束置位:新档 false → settle 置 true 恰一次(幂等) - DirAccess.remove_absolute("user://smoke_node_map_save.json") # 测试隔离——上轮 settle 写盘的残留档先删 - var sv = load("res://core/save_manager.gd").new("user://smoke_node_map_save.json") + var sv = SaveManagerScript.new(_bp_path + ".flag.json") sv.load_from_disk() - Kit.check("新档首次冒险布尔位 = false(默认域)", sv.is_first_adventure_done() == false) + Kit.check("新档首次冒险布尔位 = false(默认域——首局恒走固定种子 42)", + sv.is_first_adventure_done() == false) var mp = load("res://core/meta_progression.gd").new(_db, sv) mp.settle_adventure({"levels_cleared": 1, "keystone_total": 10, "cleared": false, "result": "failed"}) Kit.check("冒险收束点置位(三结局同写——写入只在收束挂点)", sv.is_first_adventure_done() == true) mp.settle_adventure({"levels_cleared": 0, "keystone_total": 0, "cleared": false, "result": "failed"}) Kit.check("置位幂等(重复收束不翻转)", sv.is_first_adventure_done() == true) + DirAccess.remove_absolute(_bp_path) + DirAccess.remove_absolute(_bp_path + ".flag.json") -# ---- 冒烟恢复执行体(LevelRunner 注入用——与 AdventureRunner.apply_rest 同式占位; +# 图数据良构前置(防空集假绿——节点数 > 1、句柄唯一、depth 与 next 键在位) +func _not_empty_graph(nodes: Array) -> bool: + if nodes.size() < 2: + return false + for node in nodes: + var n := node as Dictionary + if not n.has("id") or not n.has("depth") or not n.has("next"): + return false + return true + + +func _handles_unique(nodes: Array) -> bool: + var seen := {} + for node in nodes: + var id: int = int((node as Dictionary).get("id", -1)) + if id < 0 or seen.has(id): + return false + seen[id] = true + return true + + +func _fresh_rng(seed_val: int): + var r = load("res://core/rng_service.gd").new() + r.new_adventure(seed_val) + return r + + +# ---- 恢复执行体(LevelRunner 注入用——与 AdventureRunner.apply_rest 同式占位; # smoke_all.gd helper 的宿主自持私有副本——源文件收尾整体删除,宿主自给自足)---- func _smoke_rest_stub(pct: int, hp: int, max_hp: int) -> int: return mini(int(floor(float(max_hp) * float(pct) / 100.0)), max_hp - hp) diff --git a/game/tests/run_all.gd b/game/tests/run_all.gd index 824ecf97..a8501016 100644 --- a/game/tests/run_all.gd +++ b/game/tests/run_all.gd @@ -54,6 +54,7 @@ func _suites() -> Array: {"path": "res://tests/unit/core/treasure_items_test.gd", "expect": 0}, # 秘宝结构冻结断言面 {"path": "res://tests/unit/core/node_map_generator_test.gd", "expect": 0}, # 节点图生成器产出合格/可复现/岔路取舍/恰一店/归属锁定/战斗保底/出口结构/RNG 纯流面(卡① V1·V3·V5–V7·V9·V10) {"path": "res://tests/unit/core/node_map_growth_test.gd", "expect": 0}, # 生成图生长递增+参数无解 fail-fast 子进程通道(卡① V8/V4) + {"path": "res://tests/unit/core/level_table_validate_test.gd", "expect": 0}, # 消费侧加载校验四检+良构基底正负向(卡② T1/V1〔v1〕) {"path": "res://tests/unit/core/breakpoint_resume_test.gd", "expect": 0}, # 断点往返/续玩确定性/幂等/退出清断点 {"path": "res://tests/unit/core/curio_refresh_paid_test.gd", "expect": 0}, # 三选一刷新免费双源+GP 付费档(对齐商店 MC-1:价曲线/优先序/停点清零/钱包单口) {"path": "res://tests/unit/core/level_map_type_vis_test.gd", "expect": 0}, # 信息迷雾——类型可见性(层距 N+Boss/候选豁免+单圈+N 读表+续玩同值) @@ -86,7 +87,7 @@ func _suites() -> Array: {"path": "res://tests/unit/presentation/modal_flow_test.gd", "expect": 0}, # 模态编排纯逻辑(V6:直接 new/互斥域谓词/三例外锁/演播后排窗) # ---- L2 集成层(跨系统用例,与 unit/ 的隔离断言面分工)---- {"path": "res://tests/integration/save_rng_roundtrip_test.gd", "expect": 0}, # 存档⇄RNG 快照往返(SaveManager 迁移链 × 断点消费 × RNG 续跑 × 停点态确定性) - {"path": "res://tests/integration/node_map_templates_test.gd", "expect": 0}, # 关卡节点图生成·模板池(模板池跨系统×首冒险置位落盘——批 B 自 smoke_all 迁入,裁决六整套集成不拆) + {"path": "res://tests/integration/node_map_templates_test.gd", "expect": 0}, # 关卡节点图生成·确定性承载(生成图复现×首冒险置位落盘——批 B 自 smoke_all 迁入,裁决六整套集成不拆) {"path": "res://tests/integration/cross_system_flow_test.gd", "expect": 0}, # 跨系统真实流×落盘(RNG 事件日志落盘/专长断点往返/存档摘要集成/试玩记录仪真实结算流——批 B 转办块宿主,裁决一/二/三/七) # ---- core 四件与呈现层两件的直接单测 ---- {"path": "res://tests/unit/core/board_generator_test.gd", "expect": 0}, # 棋盘生成(尺寸合表/数字=八邻雷数/同种子逐位一致/可推理自洽) @@ -102,6 +103,7 @@ func _suites() -> Array: {"path": "res://tests/unit/presentation/battle_narrator_test.gd", "expect": 0}, # 战斗叙述(数值带入/胜负互异/收束态恒定) {"path": "res://tests/unit/presentation/type_glow_test.gd", "expect": 0}, # 未开格类型微光数据面(九型归组封闭/清单外显式失败/冻结色相只准动明度/α170·带宽cell÷8锚定/合成空间阶梯单调+模型CR≥1.20/probe API 形状) {"path": "res://tests/unit/presentation/theme_projection_test.gd", "expect": 0}, # 呈现投影与主题宿主(微光数据面/战斗日志行协议/全局主题与按钮五态/纹理过滤白名单/对比度墨色锚/元素细节源码锚/诊断钮退役源码契约/状态面板与生涯页只读投影——批 B 自 smoke_all 迁入;试玩记录仪整套按裁决七转办集成宿主) + {"path": "res://tests/unit/presentation/player_copy_sweep_test.gd", "expect": 0}, # 玩家面文案清扫宿主(迭代36 卡A——钉值表换代断言①–⑧⑩+源 token 扫描两处;直建断言族,禁字「雷」族与旧术语清零钉值) ] diff --git a/game/tests/static_scan.sh b/game/tests/static_scan.sh index 7c908d7c..0fa07111 100644 --- a/game/tests/static_scan.sh +++ b/game/tests/static_scan.sh @@ -236,4 +236,19 @@ else fi fi +# ── S13 — 第三方质量规则闸(gdstyle v0.4.0,版本钉死)── +# 规则说明自持本头注(随 S11/S12 惯例):gdstyle(github.com/atelico/gdstyle,Rust 单二进制)只开 +# quality/* 抓真缺陷(重复 load/循环内 await/循环内分配/不可达代码/自比较/字典重复键/残留 +# print 等),format/*/naming/*/order/* 与 S9/S10 职能重叠全关;扫描面=产品码三目录(与 S9 同口径, +# probe/sim/tests 测量面直构惯例不在面)。存量基线 tools/gdstyle_baseline.txt(文件|规则|消息 三元组 +# ——消息含违规对象标识,行漂移稳定),只有基线外新违规才红;基线重生成=人工专用 +# (python tools/gdstyle_gate.py --refresh),禁清基线换绿。二进制本机固定路径 +# D:/code/gdstyle/gdstyle.exe(不入库),GDSTYLE_BIN 环境变量覆盖;版本与钉定版不符即红(升级须重测)。 +if python tools/gdstyle_gate.py; then + echo "[static_scan] S13 ok" +else + echo "[static_scan] S13 FAIL: 基线外新违规或二进制/版本/扫描面问题(清单见上)" + fail=1 +fi + exit $fail diff --git a/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd b/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd index 6a6b0428..066a3ad0 100644 --- a/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd +++ b/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd @@ -290,7 +290,7 @@ func _smoke_encounter_roll() -> void: # 战斗结算 roll_lo 下限 + 敌人系统 spawn count<1 fail-fast 双闸;单深度单种子是盲区,全域扫描才钉死不变式) var depths: Array = [] for node in _level_battle_nodes(1): - depths.append(int(node.get("node_depth", -1))) + depths.append(int(node.get("depth", -1))) var scan_bad := 0 var scan_total := 0 for i in range(12): @@ -317,22 +317,11 @@ func _smoke_encounter_roll() -> void: # 节点类型 → 敌人表 row_kind(非 Boss 战斗位恒 battle——深度带批寻址第三维) func _row_kind_of(level_index: int, depth: int) -> String: for n in _level_active_nodes(level_index): - if int(n.get("node_depth", -1)) == depth and String(n.get("type", "")) == "boss": + if int(n.get("depth", -1)) == depth and String(n.get("type", "")) == "boss": return "boss" return "battle" -# 该关 Boss 节点深度(单终点图的终点编号——数据驱动,禁写死坐标) - - -# 该关 Boss 节点深度(单终点图的终点编号——数据驱动,禁写死坐标) -func _boss_depth(level_index: int) -> int: - for n in _level_active_nodes(level_index): - if String(n.get("type", "")) == "boss": - return int(n.get("node_depth", -1)) - return -1 - - # 首遇带数据驱动定位(契约 8):该 (L, battle, enemy_id) 的最低 depth_band,禁写死坐标数字 @@ -350,18 +339,12 @@ func _first_encounter_band(level_index: int, enemy_id: String) -> int: return best -# 模板感知取节点表(row.templates 非空取模板 0=现行固定图,保持既有断言语义) - - -# 模板感知取节点表(row.templates 非空取模板 0=现行固定图,保持既有断言语义) +# 生成图取数面(图 = f(关卡行参数档, 种子 42)——与 runner 起关同图;节点键 id/type/depth/next) func _level_active_nodes(level_index: int) -> Array: - for row in _db.level.rows: - if int(row.level_index) == level_index: - var tpls: Array = row.get("templates", []) - if not tpls.is_empty(): - return tpls[0].get("nodes", []) - return row.get("nodes", []) - return [] + var rng = load("res://core/rng_service.gd").new() + rng.set_master_seed(42) + var gen = NodeMapGenerator.new(rng) + return (gen.generate(_db.level_gen_params_row(level_index), 42) as Dictionary).get("nodes", []) func _level_battle_nodes(level_index: int) -> Array: @@ -596,6 +579,16 @@ func _smoke_skill_pool_data() -> void: "3_battle_6_hook_horror": {"attack": 6, "bite": 2, "charge": 1}, "3_battle_7_duergar": {"attack": 6, "charge": 2, "heavy": 2, "shield_bash": 2}, "3_boss_9_duergar_king": {"attack": 6, "heavy": 2, "earth_pulse": 2, "rockslide": 2}, + # 迭代 35 生成图深度扩面覆盖批八行=同关同 row_kind 顶档行的池克隆(零新池、零新技能引用): + # 战斗带顶 L1→6/L2→9/L3→13;Boss 按终点深度分带 L1→6、L2→8、L3→11/12/13 + "1_battle_6_bugbear": {"attack": 6, "charge": 2, "heavy": 2}, + "1_boss_6_ogre_chief": {"attack": 6, "heavy": 2, "harden": 2, "quake_smash": 2}, + "2_battle_9_giant_spider": {"attack": 6, "bite": 2, "charge": 1, "venom_spit": 2}, + "2_boss_8_drider": {"attack": 6, "heavy": 2, "venom_burst": 2, "web_snare": 2, "venom_curb": 2}, + "3_battle_13_duergar": {"attack": 6, "charge": 2, "heavy": 2, "shield_bash": 2}, + "3_boss_11_duergar_king": {"attack": 6, "heavy": 2, "earth_pulse": 2, "rockslide": 2}, + "3_boss_12_duergar_king": {"attack": 6, "heavy": 2, "earth_pulse": 2, "rockslide": 2}, + "3_boss_13_duergar_king": {"attack": 6, "heavy": 2, "earth_pulse": 2, "rockslide": 2}, } var pools_ok := true for row in _db.enemy.rows: @@ -608,7 +601,7 @@ func _smoke_skill_pool_data() -> void: got[String(entry.skill_id)] = int(entry.weight) if got != expect_pools[rk]: pools_ok = false - Kit.check("二十行九种池模板 = 建议池行值((L,row_kind,depth_band,enemy_id) 四元键逐行精确比对;L1 地精 9/1 · 熊地精 6/2/2 · 酋长 6/2/2;L2 熊地精 6/1/2 · 蜘蛛 6/2/1+毒吐2〔首遇带不投——迭代 24 N2〕 · 重击手 5/2/2 · 蛛化卓尔精灵 6/2/2/2〔Boss〕;L3 灰矮人 6/2/2+盾击2 · 钩爪恐魔 6/2/1 · 重装手 5/2/2+盾击2 · 石君王 6/2/2〔earth_pulse〕——全部 provisional;三 Boss 池尾 +1 门控条目〔quake_smash/venom_curb/rockslide 权重 2+unlock_hp_pct 50——迭代14④ T5〕;混编行集同带多行各一池合法——首批投放 (2,battle,5) 双行〔迭代 18 卡⑤:重击手+巢群〕,其余带键仍恰一行)", pools_ok) + Kit.check("二十八行九种池模板 = 建议池行值((L,row_kind,depth_band,enemy_id) 四元键逐行精确比对;L1 地精 9/1 · 熊地精 6/2/2 · 酋长 6/2/2;L2 熊地精 6/1/2 · 蜘蛛 6/2/1+毒吐2〔首遇带不投——迭代 24 N2〕 · 重击手 5/2/2 · 蛛化卓尔精灵 6/2/2/2〔Boss〕;L3 灰矮人 6/2/2+盾击2 · 钩爪恐魔 6/2/1 · 重装手 5/2/2+盾击2 · 石君王 6/2/2〔earth_pulse〕——全部 provisional;三 Boss 池尾 +1 门控条目〔quake_smash/venom_curb/rockslide 权重 2+unlock_hp_pct 50——迭代14④ T5〕;混编行集同带多行各一池合法——首批投放 (2,battle,5) 双行〔迭代 18 卡⑤:重击手+巢群〕;迭代 35 覆盖批八行为顶档池克隆,逐行同检)", pools_ok) var basic_count_ok := true for row in _db.enemy.rows: var n := 0 diff --git a/game/tests/unit/core/adventure_feats_test.gd b/game/tests/unit/core/adventure_feats_test.gd index a19ac9d1..379390e1 100644 --- a/game/tests/unit/core/adventure_feats_test.gd +++ b/game/tests/unit/core/adventure_feats_test.gd @@ -58,7 +58,7 @@ func _smoke_adventure_api() -> void: adr.level_started.connect(_api_mark_started.bind(sig_seq)) adr.choices_presented.connect(_api_mark_choices.bind(sig_seq, obs)) adr.node_entered.connect(_api_mark_node.bind(sig_seq)) - Kit.check("冒险启动受理(主种子注入恰一次——state=running;first_script=固定模板 0——精确路径断言需要现行图,零抽签)", adr.start_adventure(42, {"first_script": true})) + Kit.check("冒险启动受理(主种子注入恰一次——state=running;种子 42=首局固定种子生成图——精确路径断言需要确定图(卡② 代差))", adr.start_adventure(42)) Kit.check("进行中重复 start_adventure 拒绝(重开须待结算完毕)", not adr.start_adventure(43)) Kit.check("停点前移次序契约:level_started 先于入口候选呈现(反序选路口会被呈现层 runner 空防御丢弃)", sig_seq.size() == 2 and String(sig_seq[0]) == "level_started" and String(sig_seq[1]) == "choices_presented") Kit.check("入口停点:选路态 + 入口单候选 + 首战未开(呈现零 RNG 消耗——pick 前零战斗域)", @@ -108,26 +108,27 @@ func _api_mark_node(_node: Dictionary, _info: Dictionary, seq: Array) -> void: func _smoke_adventure() -> void: print("--- 冒险结构验收(三关序列 · 大奖励 · 摘要五件事 · 账本封闭)---") var autoplay = load("res://sim/level_autoplay.gd") - # ① 表与覆盖义务(序列关数 = 行集关数;行集内每个战斗/Boss 节点跨表精确行集——覆盖义务断言; - # enemy 侧键域 = (level_index, row_kind, depth_band)——战斗节点查 battle 键域、Boss 节点查 boss 键域, - # 同带多行合法,带键聚合天然兼容〔fv24〕) + # ① 表与覆盖义务(序列关数 = 行集关数;节点源 = 参数档在固定种子下的生成图, + # 每个战斗/Boss 节点经 ConfigDB 真实解析链查敌行集与棋盘行——顶 clamp 消解禁〔fv24〕) Kit.check("序列关数 = 行集关数(三关正式序列生效)", int(_db.level.rows.size()) == 3) - var bg_keys := {} - for row in _db.board_gen.rows: - bg_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true - var en_keys := {} - for row in _db.enemy.rows: - en_keys["%d_%s_%d" % [int(row.level_index), String(row.get("row_kind", "")), int(row.get("depth_band", -1))]] = true - var miss := 0 - for row in _db.level.rows: - for n in _level_active_nodes(int(row.level_index)): + var cov_miss := 0 + var cov_scan := 0 + for li in range(1, int(_db.level.rows.size()) + 1): + for n in _level_active_nodes(li): var nt := String(n.get("type", "")) if nt != "battle" and nt != "boss": continue - var key := "%d_%s_%d" % [int(row.level_index), nt, int(n.get("node_depth", -1))] - if not bg_keys.has("%d_%d" % [int(row.level_index), int(n.get("node_depth", -1))]) or not en_keys.has(key): - miss += 1 - Kit.check("行集内每个战斗/Boss 节点在 board_gen + enemy 有精确行集(缺失 %d——带键 (L,row_kind,depth_band))" % miss, miss == 0) + var d: int = int(n.get("depth", -1)) + cov_scan += 1 + var rk := "boss" if nt == "boss" else "battle" + var rs: Array[Dictionary] = _db.resolve_enemy_rowset(li, d, rk) + if rs.is_empty() or int(rs[0].get("depth_band", -1)) < d: + cov_miss += 1 + if not (_db.resolve_board_gen(li, d) as Dictionary).is_empty(): + continue + cov_miss += 1 + Kit.check("生成图每个战斗/Boss 节点经解析链命中敌行集与棋盘行(扫描 %d 节点,缺失 %d)〔fv24〕" % [cov_scan, cov_miss], + cov_scan > 0 and cov_miss == 0) # ② 满配通关 + ④ 三层奖励兑付(三选一恰 2 次 · 回血封顶 · 钥石逐杀 · 候选零重复) var st: Dictionary = autoplay.run_adventure(_db, 42, 900, 400) var sum: Dictionary = st["summary"] @@ -151,7 +152,44 @@ func _smoke_adventure() -> void: elif (ev as Dictionary).has("curio_resolved"): resolves.append((ev as Dictionary)["curio_resolved"]) Kit.check("通关三选一恰呈现 2 次、恰兑付 2 次(L1/L2 后——末关不发;resolves 含节点回执另计)", picks.size() == 2 and resolves.size() == 2 + node_picks) - Kit.check("奖励节点三选一 ≥1 次(L1 链必经 1 个;L2/L3 图可选路经若干——实际经停 %d)" % node_picks, node_picks >= 1) + # 奖励节点在生成图里是可选路位(旧单链模板的「L1 必经 1 个」形状前提随模板消亡)—— + # 接线证明拆两半:图侧每关含 ≥1 奖励节点,行进侧种子扫描内确有经停(扫描零经停=三选一接线不可达) + var reward_levels := 0 + for li in range(1, int(_db.level.rows.size()) + 1): + var rc := 0 + for n in _level_active_nodes(li): + if String(n.get("type", "")) == "reward": + rc += 1 + if rc >= 1: + reward_levels += 1 + Kit.check("三关生成图各含 ≥1 奖励节点(节点三选一候选位在图)〔v4〕", reward_levels == 3) + # 行进侧两断言走真实选路入口(满配赢下途中战→岔口按偏好显式点选):奖励支路经停即呈三选一、 + # 进战载荷带节点深度——确定性评分策略恒选推进支路,奖励位不在扫描路由上,故显式点选验证接线 + var rw: Dictionary = _drive_to_pref(autoplay, "reward") + Kit.check("奖励分支点选→三选一候选可取且停点在途(候选 %s 件)" % str((rw.get("cands", []) as Array).size()), + bool(rw.get("pending", false)) and (rw.get("cands", []) as Array).size() >= 1) + var rw_pc: Dictionary = {} + for c in (rw.get("cands", []) as Array): + var cd := c as Dictionary + if String(cd.get("kind", "")) != "feat_ticket": + rw_pc = cd + break + if rw_pc.is_empty() and not (rw.get("cands", []) as Array).is_empty(): + rw_pc = (rw.get("cands", []) as Array)[0] as Dictionary + var rw_pick_ok := false + if not rw_pc.is_empty() and rw.get("adr") != null: + rw_pick_ok = rw["adr"].apply_node_curio(String(rw_pc.get("kind", "")), String(rw_pc.get("tier", "")), String(rw_pc.get("feat_id", ""))) + Kit.check("奖励三选一点选兑付受理(件=%s)" % String(rw_pc.get("kind", "")), rw_pick_ok) + var enc_run: Dictionary = _drive_to_pref(autoplay, "battle") + var enc_probe: Dictionary = enc_run.get("enc", {}) as Dictionary + var node_probe: Dictionary = enc_run.get("node", {}) as Dictionary + # 定位信息两样(契约 2):战斗侧收到的 node_depth=节点深度,且命中节点句柄≠深度—— + # 变异「enc 取句柄」必红(V6) + Kit.check("进战载荷两样=关卡序号+节点深度(实测 depth=%s 句柄=%s)〔v6〕" % [str(enc_probe.get("node_depth", -1)), str(node_probe.get("id", -1))], + not enc_probe.is_empty() + and int(enc_probe.get("node_depth", -1)) == int(node_probe.get("depth", -1)) + and int(enc_probe.get("level_index", -1)) == 1 + and int(node_probe.get("id", -1)) != int(node_probe.get("depth", -1))) var node_resolves := 0 for r0 in resolves: if String((r0 as Dictionary).get("context", "")) == "reward_node": @@ -260,7 +298,7 @@ func _smoke_adventure() -> void: # ⑥ 重复宣布拒绝(settle 后再调 _on_level_finished——摘要零变化) var adr6 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(7)) adr6.set_hero_override(900, 400) - Kit.check("重复拒绝场景:冒险启动(seed 7——固定模板 0 同上)", adr6.start_adventure(7, {"first_script": true})) + Kit.check("重复拒绝场景:冒险启动(seed 7——生成图确定同上)", adr6.start_adventure(7)) adr6.runner().pick_next_node(int(adr6.runner().next_candidate_ids()[0])) # 入口停点 pick 进首战 var b6 = adr6.runner().current_battle() var st6 := _adv_st() @@ -284,13 +322,13 @@ func _smoke_adventure() -> void: Kit.check("每战完成入账恰一次(gain %d = battles %d——GP 袋件入账单列)" % [gains, int(st["battles"])], gains == int(st["battles"])) Kit.check("钱包 clear 恰一次且为末事件(关卡收束不清——GP 冒险收束清零归冒险结构)", clears == 1 and wallet.size() > 0 and String(wallet[wallet.size() - 1]["event"]) == "clear") var shop_seq: Array = st["shop_gp_seq"] - Kit.check("L2 商店房到达余额 > 0(%d——L1 积累跨关延续;两关商店房必经各 1)" % (shop_seq[0] if shop_seq.size() > 0 else -1), shop_seq.size() == 2 and int(shop_seq[0]) > 0) + Kit.check("三关商店房各到达一次(每关入口→终点路径恰穿一店的驱动投影)且首达余额 > 0(%d——GP 跨关延续)" % (shop_seq[0] if shop_seq.size() > 0 else -1), shop_seq.size() == 3 and int(shop_seq[0]) > 0) # ⑧ 加算区同账(手工场景:L2 商店购买 → 三选一直调——同一本账累加 + 部分回血封顶) var same := {} for s in range(42, 92): var a8 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(s)) a8.set_hero_override(900, 400) - if not a8.start_adventure(s, {"first_script": true}): # 固定模板 0——种子扫描语义按现行图 + if not a8.start_adventure(s): # 种子扫描语义按生成图(同种子同图) continue if _adv_drive_to_shop(a8, _adv_st(), autoplay): same = {"a": a8, "s": s} @@ -335,7 +373,7 @@ func _smoke_adventure() -> void: # ⑩ 放弃(战斗中拒绝 · 非战斗期受理同失败型结算) var a10 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(15)) a10.set_hero_override(900, 400) - Kit.check("放弃场景:冒险启动(seed 15——固定模板 0 同上)", a10.start_adventure(15, {"first_script": true})) + Kit.check("放弃场景:冒险启动(seed 15——生成图确定同上)", a10.start_adventure(15)) Kit.check("放弃场景:入口停点 pick 进首战(停点前移批 2026-09-23 驱动口径)", a10.runner().pick_next_node(int(a10.runner().next_candidate_ids()[0]))) Kit.check("战斗中 abandon 拒绝(战斗域单事务在飞)", not a10.abandon()) var b10 = a10.runner().current_battle() @@ -348,7 +386,7 @@ func _smoke_adventure() -> void: # ⑪ 池耗即枯竭 + 池收缩 + 满血回 0(TP0——white-box:耗尽 attack/ap 令牌后候选零出现; # 无种类次数上限,「达上限剔除」语义由牌库耗尽承载) var a11 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(11)) - Kit.check("剔牌库/替换场景:冒险启动(seed 11——道具解锁集只含 wide_scout_band)", a11.start_adventure(11, {"first_script": true, "treasure_unlocked": {"wide_scout_band": true}})) + Kit.check("剔牌库/替换场景:冒险启动(seed 11——道具解锁集只含 wide_scout_band)", a11.start_adventure(11, {"treasure_unlocked": {"wide_scout_band": true}})) for di in range(a11._curio_deck.size() - 1, -1, -1): var dk := String((a11._curio_deck[di] as Dictionary).get("kind", "")) if dk == "attack" or dk == "ap": @@ -370,7 +408,7 @@ func _smoke_adventure() -> void: Kit.check("池收缩——40 次重抽零 aux 候选(停点渠道 aux 卡退役,迭代 11 第①批 T7)", not treasure_seen) # 满血回 0 合法(通关回血封顶口径) var a12 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(12)) - a12.start_adventure(12, {"first_script": true}) + a12.start_adventure(12) var cap := {"payload": {}} a12.curio_choice_presented.connect(func(p): cap["payload"] = p) var max_full := int(_db.hero.max_hp) + int(a12._base_override.get("max_hp", 0)) @@ -379,7 +417,7 @@ func _smoke_adventure() -> void: Kit.check("满血通关回血 0 合法(封顶口径——heal_gained=0 非负溢出)", int(cap["payload"].get("heal_gained", -1)) == 0) # ⑬ 奖励节点共享池(同库同池同摘牌;TP0:attack 令牌耗尽后节点域同样零出现) var a13 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(13)) - Kit.check("节点共享池场景:冒险启动(seed 13)", a13.start_adventure(13, {"first_script": true, "treasure_unlocked": {"wide_scout_band": true}})) + Kit.check("节点共享池场景:冒险启动(seed 13)", a13.start_adventure(13, {"treasure_unlocked": {"wide_scout_band": true}})) for di13 in range(a13._curio_deck.size() - 1, -1, -1): if String((a13._curio_deck[di13] as Dictionary).get("kind", "")) == "attack": a13._curio_deck.remove_at(di13) @@ -411,7 +449,7 @@ func _smoke_feats() -> void: Kit.check("feat_table 已加载(30 件=基础 27+迁入 2+卡② 完美保险 1——迭代 31 卡② 换代)", _db.feats != null and (_db.feats.rows as Array).size() == 30) Kit.check("feat_table.validate 零违例(四级 rarity/cap 域/trigger 词表/行键白名单全过)", _db.feats.validate().is_empty()) var a_r0 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(71)) - Kit.check("专长带场景:冒险启动(seed 71)", a_r0.start_adventure(71, {"first_script": true})) + Kit.check("专长带场景:冒险启动(seed 71)", a_r0.start_adventure(71)) var feat_tokens := 0 var tier_counts := {} for t in a_r0._curio_deck: @@ -465,7 +503,7 @@ func _smoke_feats() -> void: var live_rows: FeatTable = _db.feats _db.feats = rt var a_r = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(72)) - Kit.check("专长带场景:冒险启动(seed 72——注入 5 行临时表)", a_r.start_adventure(72, {"first_script": true})) + Kit.check("专长带场景:冒险启动(seed 72——注入 5 行临时表)", a_r.start_adventure(72)) var deck_relic := 0 for t in a_r._curio_deck: if String((t as Dictionary).get("kind", "")) == "relic": @@ -485,7 +523,7 @@ func _smoke_feats() -> void: Kit.check("节点渠道零专长(60 次重抽零 relic——专长在非 Boss 渠道只以专长凭证形态出现)", not node_relic_seen) # T5 专长凭证行为断言(A5 拿到即开 / A6 保底计数——契约④四口径;独立 runner 防污染 a_r 牌库) var a_t = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(91)) - a_t.start_adventure(91, {"first_script": true}) + a_t.start_adventure(91) var stock_before := {} for kk in ["attack", "gp", "heal"]: stock_before[kk] = a_t.pool_stock(kk) @@ -769,6 +807,50 @@ func _adv_drive_to_shop(a, st: Dictionary, autoplay) -> bool: # 手工场景状态袋(_drive_level/_autoplay_battle 消费的最小字段族——run_adventure st 的同构子集) +# 真实选路驱动到偏好支路:赢下途中战斗,遇候选含目标类型即显式点选(reward=首个奖励节点、 +# battle=首个句柄≠深度的战斗节点);返回 {node, enc, cands, pending, adr} +func _drive_to_pref(autoplay, prefer: String) -> Dictionary: + var adr = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(42)) + adr.set_hero_override(900, 400) + var out := {"node": {}, "enc": {}, "cands": [] as Array, "pending": false, "adr": adr} + if not adr.start_adventure(42): + return out + var r = adr.runner() + var st := _adv_st() + var guard := 30 + while r != null and guard > 0: + guard -= 1 + if r.awaiting_choice(): + var hit: Dictionary = {} + for c in r.next_candidates(): + var cd := c as Dictionary + if prefer == "reward" and String(cd.get("type", "")) == "reward": + hit = cd + break + if prefer == "battle" and String(cd.get("type", "")) == "battle" and int(cd.get("id", -1)) != int(cd.get("depth", -1)): + hit = cd + break + if not hit.is_empty(): + r.pick_next_node(int(hit.get("id", -1))) + out["node"] = hit + if prefer == "battle": + out["enc"] = r.current_battle().encounter + else: + var cands: Array = (adr.node_curio_candidates() as Dictionary).get("candidates", []) as Array + adr.present_node_curio(cands) + out["cands"] = cands + out["pending"] = not (adr._node_curio_pending as Dictionary).is_empty() + return out + var ids: Array = r.next_candidate_ids() + if ids.is_empty(): + return out + r.pick_next_node(int(ids[0])) + continue + var b = r.current_battle() + if b == null: + return out + autoplay._autoplay_battle(r, b, st, null) + return out func _adv_st() -> Dictionary: return {"result": "", "hp_left": 0, "battles": 0, "rounds": 0, "hero_dmg": 0, "rest_healed": 0, "hp_before_rest": -1, "defeat_node_index": -1, "defeat_node_type": "", @@ -809,12 +891,7 @@ func _fresh_rng(seed_val: int): # ---- ① GP 冒险钱包·到账 ---- -# 模板感知取节点表(row.templates 非空取模板 0=现行固定图,保持既有断言语义) +# 生成图取数面(图 = f(关卡行参数档, 种子 42)——与 runner 起关同图;节点键 id/type/depth/next) func _level_active_nodes(level_index: int) -> Array: - for row in _db.level.rows: - if int(row.level_index) == level_index: - var tpls: Array = row.get("templates", []) - if not tpls.is_empty(): - return tpls[0].get("nodes", []) - return row.get("nodes", []) - return [] + var gen = NodeMapGenerator.new(_fresh_rng(42)) + return (gen.generate(_db.level_gen_params_row(level_index), 42) as Dictionary).get("nodes", []) diff --git a/game/tests/unit/core/board_gen_reward_test.gd b/game/tests/unit/core/board_gen_reward_test.gd index d9439042..e26e23a6 100644 --- a/game/tests/unit/core/board_gen_reward_test.gd +++ b/game/tests/unit/core/board_gen_reward_test.gd @@ -134,9 +134,24 @@ func _smoke_board_generator() -> void: var row = _db.resolve_board_gen(L, 0) Kit.check("BoardGen 行解析 (%d,0)" % L, not row.is_empty()) if L == 2: - # R1 ceil 口径:无精确行向上取整保证偏难——L2 深度 3 为恢复点无行,解析到深度 4 + # 迭代 35 生成图深度扩面覆盖批随动(声明):旧口径「(2,3) 无精确行→ceil 上取到 4」的判据对象 + # 是模板时代键域缺口;覆盖批补齐 0–13 后 (2,3) 有精确行,生产表上不再存在可走 ceil 的空位。 + # 判据不丢——精确命中改判在册,ceil 能力用「临时摘除该行」的夹具面保留,随后原样还原。 + var exact_row: Dictionary = _db.resolve_board_gen(2, 3) + Kit.check("BoardGen 精确命中 (2,3)(覆盖批键域补齐)", int(exact_row.get("node_depth", -1)) == 3) + var gap_idx := -1 + for i in _db.board_gen.rows.size(): + if int(_db.board_gen.rows[i].get("level_index", -1)) == 2 and int(_db.board_gen.rows[i].get("node_depth", -1)) == 3: + gap_idx = i + break + var saved: Dictionary = _db.board_gen.rows[gap_idx] + var rows_before: int = _db.board_gen.rows.size() + _db.board_gen.rows.remove_at(gap_idx) var ceil_row: Dictionary = _db.resolve_board_gen(2, 3) - Kit.check("R1 ceil 解析 (2,3)→(2,4)(向上取整偏难)", int(ceil_row.get("node_depth", -1)) == 4) + Kit.check("R1 ceil 能力不失(夹具摘除 (2,3)→向上取整到 4·偏难口径)", int(ceil_row.get("node_depth", -1)) == 4) + _db.board_gen.rows.insert(gap_idx, saved) + Kit.check("夹具表原样还原(行数与解析回到覆盖批现值)", + _db.board_gen.rows.size() == rows_before and int(_db.resolve_board_gen(2, 3).get("node_depth", -1)) == 3) var b1 = board_generator.generate(L, 0, _rng.open_stream("board_gen"), _db) Kit.check("L%d 生成产物非空" % L, b1 != null) if b1 == null: diff --git a/game/tests/unit/core/breakpoint_resume_test.gd b/game/tests/unit/core/breakpoint_resume_test.gd index 92323340..e6eba55c 100644 --- a/game/tests/unit/core/breakpoint_resume_test.gd +++ b/game/tests/unit/core/breakpoint_resume_test.gd @@ -147,8 +147,52 @@ func _init() -> void: Kit.check_quiet(adr_i.runner().pick_next_node(int(adr_i.runner().next_candidate_ids()[0])), "P6 续玩选路进首战") Kit.check_quiet(adr_i.runner().current_battle().attune_has("morning_order"), "P6 回局首战装配集仍含 morning_order(停点往返载体态一致)") + # ---- Part 7:生成图换代断言面(快照键换代、停点句柄往返、续玩按种子+参数档重生成同图)---- + var rng_j = load("res://core/rng_service.gd").new() + var sm_j = SaveManagerScript.new(_bp_path) + var adr_j = AdventureRunnerScript.new(db, rng_j) + adr_j.set_save_manager(sm_j) + Kit.check_quiet(adr_j.start_adventure(910042, params), "P7 冒险启动(seed 910042——生成图开局)") + var runner_j = adr_j.runner() + var graph_j: Array = runner_j.nodes() + var bp_j: Dictionary = sm_j.breakpoint_snapshot() + Kit.check_quiet(not bp_j.has("template_pick"), + "P7 断点快照无已抽模板键(模板链路退役——件核面在档)〔v9〕") + Kit.check_quiet(int(bp_j.get("graph_gen_v2", 0)) == 1, + "P7 断点含生成图代差标记键(换代承载)〔v8〕") + var entry_id: int = int(runner_j.next_candidate_ids()[0]) + var mid_id: int = _first_out_id(graph_j, entry_id) + var bp_mid: Dictionary = bp_j.duplicate(true) + bp_mid["node_id"] = mid_id + bp_mid["visited_ids"] = [entry_id] + var adr_m = AdventureRunnerScript.new(db, load("res://core/rng_service.gd").new()) + Kit.check_quiet(adr_m.resume_adventure(bp_mid, params), "P7 中途停点断点续玩受理(句柄落档形态)") + var runner_m = adr_m.runner() + Kit.check_quiet(int(runner_m.current_node_id()) == mid_id, + "P7 停点句柄回读一致(图内 id 单空间寻址)〔v8〕") + Kit.check_quiet(runner_m.visited_ids() == [entry_id], "P7 已到节点序按句柄往返一致") + Kit.check_quiet(runner_m.nodes() == graph_j, "P7 续玩重生成图与中断前逐位同图(断点不存图快照)〔v7〕") + var adr_l = AdventureRunnerScript.new(db, load("res://core/rng_service.gd").new()) + var bp_legacy: Dictionary = bp_mid.duplicate(true) + bp_legacy.erase("graph_gen_v2") + Kit.check_quiet(adr_l.resume_adventure(bp_legacy, params) == false, + "P7 缺生成图代差标记键的旧断点显式拒绝(禁按新句柄空间静默回放)〔v8〕") + var adr_n = AdventureRunnerScript.new(db, load("res://core/rng_service.gd").new()) + Kit.check_quiet(adr_n.start_adventure(910042, params) and adr_n.runner().nodes() == graph_j, + "P7 同种子重开局同图(开局入口复现承载)〔v7〕") + _clear_disk() - quit(Kit.finish("breakpoint_resume_test", "PASS — 断点往返/续玩确定性/幂等/退出清断点/装配存档回归 全绿")) + quit(Kit.finish("breakpoint_resume_test", "PASS — 断点往返/续玩确定性/幂等/退出清断点/装配存档回归/生成图换代 全绿")) + + +# 句柄寻址夹具辅助:取生成图中指定句柄节点的首个出边目标(构造中途停点落档形态) +func _first_out_id(nodes: Array, node_id: int) -> int: + for node in nodes: + var n := node as Dictionary + if int(n.get("id", -1)) == node_id: + var outs: Array = n.get("next", []) + return int(outs[0]) if not outs.is_empty() else -1 + return -1 func _board_fingerprint(b) -> String: diff --git a/game/tests/unit/core/config_db_cross_validator_test.gd b/game/tests/unit/core/config_db_cross_validator_test.gd index f3fac232..e2b7731c 100644 --- a/game/tests/unit/core/config_db_cross_validator_test.gd +++ b/game/tests/unit/core/config_db_cross_validator_test.gd @@ -16,7 +16,6 @@ func _init() -> void: _test_all_links_valid() _test_dangling_skill_id() _test_cast_side_mismatch() - _test_level_node_band_rules() quit(Kit.finish("config_db_cross_validator_test")) @@ -83,61 +82,3 @@ func _test_cast_side_mismatch() -> void: var status := _status([{"effect_id": "st_hero_side", "target_side": "hero"}]) var errs: Array = ValidatorScript.new().validate_skill_pool_links(skills, enemies, status) Kit.check("buff 挂主角侧状态被报(施放类 side 守卫)", _cats(errs).has("cast_side")) - - -# ---- ④ 节点 × 敌人表带寻址(单终点零误报/同带双 Boss 报错/顶 clamp 消解报错)---- -const LevelTableScript := preload("res://data/schemas/level_table.gd") -const BoardGenTableScript := preload("res://data/schemas/board_gen_table.gd") -const RewardTableScript := preload("res://data/schemas/reward_table.gd") -const SettlementTableScript := preload("res://data/schemas/settlement_table.gd") - - -func _loaded(level_nodes: Array, bg_depths: Array, boss_bands: Array, battle_bands: Array) -> Dictionary: - var lv := LevelTableScript.new() - lv.rows.assign([{"level_index": 1, "display_name": "夹具关", "templates": [{"nodes": level_nodes}]}]) - var bg := BoardGenTableScript.new() - var bg_rows: Array = [] - for d in bg_depths: - bg_rows.append({"level_index": 1, "node_depth": int(d), "board_width": 10}) - bg.rows.assign(bg_rows) - var en := EnemyTableScript.new() - var en_rows: Array = [] - for b in battle_bands: - en_rows.append({"level_index": 1, "row_kind": "battle", "depth_band": int(b), "enemy_id": "goblin", - "unit_count": 1, "hp": 10, "atk": 2, "pool": [{"skill_id": "attack", "weight": 9}]}) - for b in boss_bands: - en_rows.append({"level_index": 1, "row_kind": "boss", "depth_band": int(b), "enemy_id": "ogre_chief", - "unit_count": 1, "hp": 10, "atk": 2, "pool": [{"skill_id": "attack", "weight": 9}]}) - en.rows.assign(en_rows) - var st := SettlementTableScript.new() - st.source_label_map.assign({"safe_cell": "a", "seal_hit": "b", "mine_hit": "c", "clear_reward": "d", - "hero_attack": "e", "enemy_action": "f", "seal_miss": "g"}) - return {"status_effects": StatusTableScript.new(), "reward": RewardTableScript.new(), - "settlement": st, "level": lv, "board_gen": bg, "enemy": en} - - -func _cross_cats(loaded: Dictionary, needle: String) -> Array[String]: - var out: Array[String] = [] - ValidatorScript.new().cross_validate(loaded, - func(_sev, cat, msg) -> void: - if String(msg).contains(needle): - out.append(String(cat))) - return out - - -func _test_level_node_band_rules() -> void: - # 正向:单终点三关同构(battle 节点落 band 0、boss 节点精确落顶带)→ ④ 零误报 - var ok_nodes := [{"type": "battle", "node_depth": 0}, {"type": "boss", "node_depth": 9}] - var ok_cats := _cross_cats(_loaded(ok_nodes, [0, 9], [9], [0]), "level 1") - Kit.check("单终点图 ④ 敌侧带寻址零误报〔fv17〕", ok_cats.is_empty()) - # 负向:一路两终点落同一 Boss 带(深度 3/4 都落带 4)→ rowset_rule - var same_band_nodes := [{"type": "battle", "node_depth": 0}, - {"type": "boss", "node_depth": 3}, {"type": "boss", "node_depth": 4}] - var same_band_cats := _cross_cats(_loaded(same_band_nodes, [0, 3, 4], [4, 9], [0]), "同带") - Kit.check("同关双终点共带报 rowset_rule〔fv15〕", - same_band_cats.has("rowset_rule")) - # 负向:Boss 节点深度高于顶带键(顶 clamp 消解)→ level_node_dangling - var over_nodes := [{"type": "battle", "node_depth": 0}, {"type": "boss", "node_depth": 10}] - var over_cats := _cross_cats(_loaded(over_nodes, [0, 10], [9], [0]), "顶 clamp 消解") - Kit.check("Boss 节点越顶带报 level_node_dangling〔fv16〕", - over_cats.has("level_node_dangling")) diff --git a/game/tests/unit/core/curio_refresh_paid_test.gd b/game/tests/unit/core/curio_refresh_paid_test.gd index de765073..26392adc 100644 --- a/game/tests/unit/core/curio_refresh_paid_test.gd +++ b/game/tests/unit/core/curio_refresh_paid_test.gd @@ -53,12 +53,12 @@ func _init() -> void: Kit.check_quiet(lr.spend_gp(20) == true and int(lr.gp) == 10, "足额支出扣账(30→10)") # ---- ⑥ Y 断点随存(缺陷锚:续玩自参数面整额重注入=用完 Y 退菜单再续玩满血复活)---- - var bp_y := {"level_index": 1, "hero_hp": 10, "attune_refresh_y": 2} + var bp_y := {"level_index": 1, "hero_hp": 10, "attune_refresh_y": 2, "graph_gen_v2": 1} var adr_r = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new()) Kit.check_quiet(adr_r.resume_adventure(bp_y, {"hero_override": {}, "attune_refresh_y": 3}), "续玩受理(最小断点+参数面)") Kit.check_quiet(adr_r._attune_refresh_y == 2, "断点有键=Y 余量随断点还原(花 1 剩 2——不再整额重注入=满血复活堵死)") var adr_o = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new()) - Kit.check_quiet(adr_o.resume_adventure({"level_index": 1, "hero_hp": 10}, {"hero_override": {}, "attune_refresh_y": 3}), "旧断点续玩受理(快照缺键)") + Kit.check_quiet(adr_o.resume_adventure({"level_index": 1, "hero_hp": 10, "graph_gen_v2": 1}, {"hero_override": {}, "attune_refresh_y": 3}), "旧断点续玩受理(快照缺键)") Kit.check_quiet(adr_o._attune_refresh_y == 3, "旧断点缺键=回退参数面整额注入(旧档行为零变化)") quit(Kit.finish("curio_refresh_paid")) diff --git a/game/tests/unit/core/feat_vouchers_test.gd b/game/tests/unit/core/feat_vouchers_test.gd index 0179721c..f529bf49 100644 --- a/game/tests/unit/core/feat_vouchers_test.gd +++ b/game/tests/unit/core/feat_vouchers_test.gd @@ -22,7 +22,7 @@ func _init() -> void: quit(1) return var a = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new()) - if not a.start_adventure(301, {"first_script": true}): + if not a.start_adventure(301): Kit.check_quiet(false, "V9 前置:冒险启动") quit(Kit.finish("feat_vouchers")) return diff --git a/game/tests/unit/core/level_map_type_vis_test.gd b/game/tests/unit/core/level_map_type_vis_test.gd index b3ca3d97..50762634 100644 --- a/game/tests/unit/core/level_map_type_vis_test.gd +++ b/game/tests/unit/core/level_map_type_vis_test.gd @@ -29,61 +29,71 @@ func _init() -> void: # ---- 夹具 ---- -# 图形态夹具(id = node_depth):单干 0→1→2→3→4→5→6→7 加一条 6→3 跨层跳边。 -# BFS 层:0=0/1=1/2=1/3=2/4=3/5=4/6=5/7=6——锚 6 时候选 3 层距 3 = N+1。 +# 节点构造器(生成图口径:id=图内唯一句柄、depth=自入口最长路径步数、next=出边句柄数组) +static func _gn(id: int, depth: int, type_: String, next_ids: Array) -> Dictionary: + return {"id": id, "type": type_, "depth": depth, "next": next_ids} + + +# 图形态夹具(id = 图内句柄,depth = 自入口沿边严格前进的深度):干 0→1→{2,8},2→3, +# 8→4→5→6,长跳边 6→3(与 2→3 同落节点 3,目标 depth 严格大于源——生成图连线铁律), +# 3 经尾链 9→10→11 汇入终点 7(7 的 depth = 自入口最长路径步数 10)。 +# BFS 层:0=0/1=1/2=1/3=2/4=3/5=4/6=5/7=6(8=2/9=3/10=4/11=5)——锚 6 时候选 3 层距 3 = N+1。 func _graph_nodes() -> Array: return [ - {"node_depth": 0, "type": "battle", "next": [1, 2]}, - {"node_depth": 1, "type": "battle", "next": [2]}, - {"node_depth": 2, "type": "reward", "next": [3]}, - {"node_depth": 3, "type": "rest", "next": [4]}, - {"node_depth": 4, "type": "battle", "next": [5]}, - {"node_depth": 5, "type": "battle", "next": [6]}, - {"node_depth": 6, "type": "battle", "next": [7, 3]}, - {"node_depth": 7, "type": "boss", "next": []}, + _gn(0, 0, "battle", [1, 2]), + _gn(1, 1, "battle", [2, 8]), + _gn(2, 2, "reward", [3]), + _gn(3, 6, "rest", [9]), + _gn(4, 3, "battle", [5]), + _gn(5, 4, "battle", [6]), + _gn(6, 5, "battle", [7, 3]), + _gn(7, 10, "boss", []), + _gn(8, 2, "battle", [4]), + _gn(9, 7, "battle", [10]), + _gn(10, 8, "battle", [11]), + _gn(11, 9, "battle", [7]), ] -# 单链夹具(无 next 键,仿 L1 现役模板形态):编号空间 = 数组下标,node_depth 只出现在战斗/Boss 上 +# 单链退化形态 = 出度 ≤1 的图(显式 next 出边、编号 = id 句柄——无数组下标隐式边) func _chain_nodes() -> Array: return [ - {"node_depth": 0, "type": "battle"}, - {"node_depth": 1, "type": "battle"}, - {"node_depth": 2, "type": "battle"}, - {"node_depth": 3, "type": "battle"}, - {"type": "reward"}, - {"type": "rest"}, - {"node_depth": 4, "type": "boss"}, + _gn(0, 0, "battle", [1]), + _gn(1, 1, "battle", [2]), + _gn(2, 2, "battle", [3]), + _gn(3, 3, "battle", [4]), + _gn(4, 4, "reward", [5]), + _gn(5, 5, "rest", [6]), + _gn(6, 6, "boss", []), ] -# validate 正样本(无跨层跳边的合法图:入口唯一、Boss 编号最大收尾、层宽 ≤5) +# 表行夹具的正样本(无跨层跳边的合法图——表加载期只核行级字段,图合法性判据见 validate_graph·卡② T1 换代) func _valid_graph_nodes() -> Array: return [ - {"node_depth": 0, "type": "battle", "next": [1, 2]}, - {"node_depth": 1, "type": "battle", "next": [3]}, - {"node_depth": 2, "type": "reward", "next": [3]}, - {"node_depth": 3, "type": "battle", "next": [4]}, - {"node_depth": 4, "type": "boss", "next": []}, + _gn(0, 0, "battle", [1, 2]), + _gn(1, 1, "battle", [3]), + _gn(2, 1, "reward", [3]), + _gn(3, 2, "battle", [4]), + _gn(4, 3, "boss", []), ] -# 白盒 runner:夹具行集注入 db.level + level_row,停点态直设(anchor_id < 0 = 入口停点) -func _runner(db, nodes: Array, anchor_id: int, candidates: Array, - graph_mode: bool = true, n: int = 2): +# 白盒 runner:夹具行集注入 db.level + level_row,停点态直设(anchor_id < 0 = 入口停点; +# 锚点与候选的编号空间 = 节点 id 句柄——runner._node_id 单点) +func _runner(db, nodes: Array, anchor_id: int, candidates: Array, n: int = 2): var t := LevelTableScript.new() t.info_visible_layers = n t.rows = [{"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30, "nodes": nodes}] db.level = t var r = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new()) r.level_row.assign({"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30, "nodes": nodes}) - r._graph_mode = graph_mode r.visited.clear() r._candidates.assign(candidates) r.node_index = -1 if anchor_id >= 0: for i in nodes.size(): - if int((nodes[i] as Dictionary).get("node_depth", -1)) == anchor_id: + if int((nodes[i] as Dictionary).get("id", -1)) == anchor_id: r.node_index = i break return r @@ -157,15 +167,15 @@ func _test_single_circle(db) -> void: Kit.check("单圈只按锚层取窗:已过近端节点 4(层距 2)仍可见〔fv7〕", vis.has(4)) -# ---- ⑤ 单链退化形态(编号空间 = 数组下标)〔fv1〕---- +# ---- ⑤ 单链退化形态(出度 ≤1 图,编号 = id 句柄)〔fv1〕---- func _test_single_chain_entry(db) -> void: var nodes := _chain_nodes() - var r = _runner(db, nodes, -1, [nodes[0]], false) + var r = _runner(db, nodes, -1, [nodes[0]]) var vis: Array = r.type_visible_ids() - Kit.check("单链退化形态:入口锚层 0,下标 2 可见〔fv1〕", vis.has(2)) - Kit.check("单链退化形态:层距 N+1(下标 3)不可见〔fv1〕", not vis.has(3)) - Kit.check("单链编号空间 = 数组下标:Boss 以末位下标 6 入集(非 node_depth 4)〔fv1〕", + Kit.check("单链退化形态:入口锚层 0,句柄 2 可见〔fv1〕", vis.has(2)) + Kit.check("单链退化形态:层距 N+1(句柄 3)不可见〔fv1〕", not vis.has(3)) + Kit.check("单链编号空间 = 图句柄 id:Boss 以句柄 6 入集(链上层距 4 的战斗节点 4 入雾)〔fv1〕", vis.has(6) and not vis.has(4)) @@ -189,20 +199,37 @@ func _test_n_from_table(db) -> void: # ---- ⑦ 续玩重建同值〔fv7〕---- func _test_resume_rebuild(db) -> void: - var nodes := _graph_nodes() var t := LevelTableScript.new() t.info_visible_layers = 2 - t.rows = [{"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30, "nodes": nodes}] + t.rows = [{"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30}] db.level = t var ledger := {"hero_hp": 10, "gp": 0} var resumed = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new()) - var ok: bool = resumed.start_level_external(1, ledger, {}, Callable(), {}, false, 0, + var ok: bool = resumed.start_level_external(1, ledger, {}, Callable(), {}, false, {"node_id": 2, "visited_ids": [0, 1, 2]}) Kit.check_quiet(ok, "续玩用例:resume_state 重建停点成功") if not ok: return Kit.check_quiet(resumed.awaiting_choice(), "续玩用例:重建后处于选路停点") - var direct = _runner(db, nodes, 2, [nodes[3]]) - direct.visited.assign([nodes[0], nodes[1], nodes[2]]) + # 图源 = 参数化生成图(图 = f(参数档, 种子))——直入对照侧与 resume 侧同吃一张生成图, + # 停点态按句柄对齐:锚 = id 2、visited = id 0/1/2、候选 = id 2 出边指向的节点 + var gnodes: Array = resumed.nodes() + var stop: Dictionary = {} + var by_id := {} + for node in gnodes: + var nid: int = int((node as Dictionary).get("id", -1)) + by_id[nid] = node + if nid == 2: + stop = node + var cand_nodes: Array = [] + for tid in (stop.get("next", []) as Array): + if by_id.has(int(tid)): + cand_nodes.append(by_id[int(tid)]) + var visited_nodes: Array = [] + for vid in [0, 1, 2]: + if by_id.has(vid): + visited_nodes.append(by_id[vid]) + var direct = _runner(db, gnodes, 2, cand_nodes) + direct.visited.assign(visited_nodes) Kit.check("续玩重建同值:resume 路径与同停点直入 type_visible_ids() 逐位一致〔fv7〕", resumed.type_visible_ids() == direct.type_visible_ids()) diff --git a/game/tests/unit/core/level_shop_economy_test.gd b/game/tests/unit/core/level_shop_economy_test.gd index af4469a8..9f4049e7 100644 --- a/game/tests/unit/core/level_shop_economy_test.gd +++ b/game/tests/unit/core/level_shop_economy_test.gd @@ -1,12 +1,14 @@ # tests/unit/core/level_shop_economy_test.gd — 关卡图与 GP 商店单元测试宿主 # 断言面 = 七套(批 B 断言迁移自 smoke_all.gd,G8 施工组): -# 关卡节点地图·线性推进(满配通关/恢复点公式/败北线/回放确定性/属性覆盖恢复) +# 关卡节点图·生成图推进(满配通关/恢复点公式/败北线/回放确定性/属性覆盖恢复) # 分支节点图(图校验正反/L2·L3 行进与行集定点/混编运行面/真图类型可见性) # GP 冒险钱包·到账(掷定窗口/关级加值) # escrow 隔离(格面兑付入缓冲/破财直验/钱包钳制不透支) # 商店房三收口(现价/加算区/余额不足拒/离开回选路/收束清零) -# 商店房图不变式(恰 1 商店房/全路径必经/跨表行集迁移) +# 商店编排性质(每终点前一位恰一店/店数=终点数/全路径恰穿一店) # 商店共享池(抽 3 买 1/乘法定价/跨渠道共库/断点扩键/终局池读数) +# 节点图源 = 参数档 × 种子的生成图——图侧断言一律经 _generated_graph 单点取数 +# (与 LevelRunner._assign_generated_graph 同参数档表、同产图路径),形状断言只写性质。 # 协议照单元侧惯例:extends SceneTree+Kit.check 主通道;--script 模式无 autoload, # ConfigDB 显式实例化构造注入(_fresh_rng 同种子隔离夹具——RngService 构造注入 runner); # 表加载失败不中止(沿被迁断言面的源语义——承 G2–G6 同款)。 @@ -15,8 +17,218 @@ extends SceneTree const Kit := preload("res://tests/unit/test_kit.gd") const ConfigDBScript := preload("res://core/config_db.gd") +const NodeMapGenScript := preload("res://core/node_map_generator.gd") + +# 现行首局种子(图侧断言的固定取图种子——与组合根首次冒险同一种子承载) +const ACTIVE_GRAPH_SEED := 42 +# 覆盖与编排断言的扫描种子面(含固定取图种子 42) +const SWEEP_SEED_FROM := 42 +const SWEEP_SEED_TO := 62 var _db +var _graph_gen = null # 生成图取数单实例(图 = f(参数档, 种子) 纯函数,复现与计数器位次无关) + + +# ---- 生成图取数面(本文件唯一实现——图侧断言一律经此,禁另起第二份取数)---- + +# 参数档 × 种子产图(与 LevelRunner._assign_generated_graph 同一产图路径与同一参数档表) +func _generated_graph(level_index: int, seed_val: int) -> Dictionary: + var params: Dictionary = _db.level_gen_params_row(level_index) + if params.is_empty(): + return {} + if _graph_gen == null: + _graph_gen = NodeMapGenScript.new(_fresh_rng(ACTIVE_GRAPH_SEED)) + var graph: Dictionary = _graph_gen.generate(params, seed_val) + if graph.is_empty() or _graph_gen.is_fatal(): + return {} + return graph + + +# 该关现行起关图(固定种子+该关参数档)——节点数组,句柄/深度/出边读面见节点字典 +func _level_active_nodes(level_index: int) -> Array: + return _generated_graph(level_index, ACTIVE_GRAPH_SEED).get("nodes", []) + + +# 句柄 → 节点(图内唯一句柄解析) +func _nodes_by_id(nodes: Array) -> Dictionary: + var out := {} + for n in nodes: + out[int(n.get("id", -1))] = n + return out + + +# 入口句柄集(无入边节点——个数 = 参数档 entry_count) +func _entry_ids(nodes: Array) -> Array: + var inbound := {} + for n in nodes: + for tid in (n.get("next", []) as Array): + inbound[int(tid)] = true + var out: Array = [] + for n in nodes: + if not inbound.has(int(n.get("id", -1))): + out.append(int(n.get("id", -1))) + return out + + +# 终点句柄集(无出边节点——恒 Boss,个数 = 参数档 exit_count) +func _terminal_ids(nodes: Array) -> Array: + var out: Array = [] + for n in nodes: + if (n.get("next", []) as Array).is_empty(): + out.append(int(n.get("id", -1))) + return out + + +# 指定型别句柄集(岔口=出度 ≥2 另走 _fork_ids) +func _ids_of_type(nodes: Array, type_name: String) -> Array: + var out: Array = [] + for n in nodes: + if String(n.get("type", "")) == type_name: + out.append(int(n.get("id", -1))) + return out + + +# 岔口句柄集(出度 ≥2 的节点——「真实岔路」现行定义) +func _fork_ids(nodes: Array) -> Array: + var out: Array = [] + for n in nodes: + if (n.get("next", []) as Array).size() >= 2: + out.append(int(n.get("id", -1))) + return out + + +# 逐节点型别计数(五型合计 = 节点数——图形状读数的唯一计数口径) +func _type_tally(nodes: Array) -> Dictionary: + var out := {"battle": 0, "boss": 0, "reward": 0, "rest": 0, "shop": 0} + for n in nodes: + var t := String(n.get("type", "")) + if out.has(t): + out[t] = int(out[t]) + 1 + return out + + +# 序列去重(保持首现序——句柄集与型别集通用) +func _uniq(values: Array) -> Array: + var out: Array = [] + for v in values: + if not out.has(v): + out.append(v) + return out + + +# 句柄序升序(候选集与图出边比对用——集合相等判据) +func _sorted_ids(ids: Array) -> Array: + var out: Array = [] + for v in ids: + out.append(int(v)) + out.sort() + return out + + +# 路径上商店节点计数(句柄序列经图解析——「恰穿一店」判据的行进面与枚举面共用口径) +func _path_shop_count(nodes: Array, path: Array) -> int: + var by_id := _nodes_by_id(nodes) + var count := 0 + for vid in path: + if String(by_id.get(int(vid), {}).get("type", "")) == "shop": + count += 1 + return count + + +# 终点深度升序表(多终点图的终点深度域——Boss 行集与带互异判据的取数面) +func _terminal_depths(nodes: Array) -> Array: + var by_id := _nodes_by_id(nodes) + var out: Array = [] + for tid in _terminal_ids(nodes): + out.append(int(by_id.get(int(tid), {}).get("depth", -1))) + out.sort() + return out + + +# 入口 BFS 层表(句柄 → 层深:入口层 0、出边逐层 +1、首达即定层——与 runner 可见性锚层同源口径) +func _bfs_layers(nodes: Array) -> Dictionary: + var by_id := _nodes_by_id(nodes) + var layers := {} + var queue: Array = [] + for eid in _entry_ids(nodes): + layers[eid] = 0 + queue.append(eid) + var i := 0 + while i < queue.size(): + var cur_id := int(queue[i]) + i += 1 + var layer := int(layers[cur_id]) + for oid in (by_id.get(cur_id, {}).get("next", []) as Array): + if not layers.has(int(oid)): + layers[int(oid)] = layer + 1 + queue.append(int(oid)) + return layers + + +# 跨表覆盖扫描(敌侧落带 ≥ 请求深度+板侧精确命中):返回 {nodes 扫描读数, enemy_bad, board_bad} +func _coverage_scan(level_index: int, nodes: Array) -> Dictionary: + var scanned := 0 + var enemy_bad := 0 + var board_bad := 0 + for n in nodes: + var d: int = int(n.get("depth", -1)) + if not _board_row_exact(level_index, d): + board_bad += 1 + var t := String(n.get("type", "")) + if t == "battle" or t == "boss": + scanned += 1 + if not _enemy_band_covers(level_index, d, "boss" if t == "boss" else "battle"): + enemy_bad += 1 + return {"nodes": scanned, "enemy_bad": enemy_bad, "board_bad": board_bad} + + +# 入口→终点全路径枚举(DFS 迭代栈——路径为句柄序;生成图无环且全员可达终点,路径有限) +func _all_entry_terminal_paths(nodes: Array) -> Array: + var by_id := _nodes_by_id(nodes) + var paths: Array = [] + var stack: Array = [] + for eid in _entry_ids(nodes): + stack.append([eid]) + while not stack.is_empty(): + var path: Array = stack.pop_back() + var cur: Dictionary = by_id[int(path[path.size() - 1])] + var outs: Array = cur.get("next", []) + if outs.is_empty(): + paths.append(path) + continue + for t in outs: + var next_path: Array = path.duplicate() + next_path.append(int(t)) + stack.append(next_path) + return paths + + +# 路径逐跳是否为图出边(含首节点为入口、末节点为终点) +func _path_in_graph(nodes: Array, path: Array) -> bool: + if path.is_empty(): + return false + var by_id := _nodes_by_id(nodes) + if not _entry_ids(nodes).has(int(path[0])): + return false + for i in range(path.size() - 1): + if not by_id.has(int(path[i])): + return false + if not (by_id[int(path[i])].get("next", []) as Array).has(int(path[i + 1])): + return false + var last: Dictionary = by_id.get(int(path[path.size() - 1]), {}) + return not last.is_empty() and (last.get("next", []) as Array).is_empty() + + +# 敌侧覆盖判据:请求深度经 band_of 落带(非 -1)——解析落带 ≥ 请求深度即等价判据(禁顶 clamp 消解) +func _enemy_band_covers(level_index: int, depth: int, kind: String) -> bool: + var rows: Array[Dictionary] = _db.resolve_enemy_rowset(level_index, depth, kind) + return not rows.is_empty() and int(rows[0].get("depth_band", -1)) >= depth + + +# 板侧覆盖判据:该深度在 board_gen 精确命中(node_depth 等值) +func _board_row_exact(level_index: int, depth: int) -> bool: + var board: Dictionary = _db.resolve_board_gen(level_index, depth) + return not board.is_empty() and int(board.get("node_depth", -1)) == depth func _init() -> void: @@ -36,38 +248,122 @@ func _init() -> void: _smoke_gp_escrow() _smoke_shop_room() _smoke_shop_graph() + _smoke_multi_terminal_settlement() _smoke_shop_pool() + _smoke_generated_coverage() quit(Kit.finish("level_shop_economy_test")) -# ---- 关卡节点地图·线性推进冒烟---- +# ---- 生成图深度扩面覆盖断言(迭代 35 卡② S5/V11——节点源=卡① 生成器产图, +# 敌侧/板侧解析一律走 ConfigDB 真实消费链,禁直读行集自算)---- + +func _smoke_generated_coverage() -> void: + var enemy_ok := true + var board_ok := true + var boss_band_ok := true + var scanned := 0 + var readings: Array = [] + for L in [1, 2, 3]: + if _db.level_gen_params_row(L).is_empty(): + Kit.check("L%d 参数档缺行(覆盖断言无源可扫)〔v11〕" % L, false) + continue + var dmin := 999 + var dmax := 0 + for seed_val in range(SWEEP_SEED_FROM, SWEEP_SEED_TO): + var nodes: Array = _generated_graph(L, seed_val).get("nodes", []) + if nodes.is_empty(): + Kit.check("L%d 种子 %d 生成图为空(覆盖扫描面空转)〔v11〕" % [L, seed_val], false) + continue + var by_id := _nodes_by_id(nodes) + for n in nodes: + var d: int = int(n.get("depth", -1)) + var kind := String(n.get("type", "")) + dmin = mini(dmin, d) + dmax = maxi(dmax, d) + scanned += 1 + # 判据=解析落带 ≥ 请求深度(等价 EnemyTable.band_of 非 -1)——行集非空不是覆盖判据, + # 越顶时 resolver 会 clamp 到顶带照样返回非空行(顶 clamp 消解正是设计禁的) + if kind == "battle" and not _enemy_band_covers(L, d, "battle"): + enemy_ok = false + elif kind == "boss" and not _enemy_band_covers(L, d, "boss"): + enemy_ok = false + if not _board_row_exact(L, d): + board_ok = false + var bands: Array = [] + for tid in _terminal_ids(nodes): + var rs: Array[Dictionary] = _db.resolve_enemy_rowset(L, int(by_id[int(tid)].get("depth", -1)), "boss") + bands.append(int(rs[0].get("depth_band", -1)) if not rs.is_empty() else -1) + for i in range(bands.size()): + for j in range(i + 1, bands.size()): + if bands[i] == bands[j]: + boss_band_ok = false + readings.append("L%d 深度域 %d–%d" % [L, dmin, dmax]) + print("[v11] 生成图覆盖量程读数:%s(扫描节点读数 %d)" % ["; ".join(readings), scanned]) + Kit.check("生成图覆盖扫描面非空(三关参数档×种子 %d–%d 共 %d 个节点读数)〔v11〕" % [SWEEP_SEED_FROM, SWEEP_SEED_TO - 1, scanned], scanned > 0) + Kit.check("生成图每个 battle/boss 节点深度的敌侧解析落带 ≥ 请求深度(band_of 非 -1·顶 clamp 消解禁)〔v11〕", enemy_ok) + Kit.check("生成图每个节点深度在 board_gen 精确命中(非空且 node_depth 等值)〔v11〕", board_ok) + Kit.check("多终点 Boss 各终点解析带键两两互异(L2/L3×20 种子)〔v11〕", boss_band_ok) + + +# ---- 关卡节点图·生成图推进冒烟(图 = 该关参数档 × 种子)---- func _smoke_level() -> void: - print("--- 关卡节点地图·线性推进(战×2→道具→战×2→恢复→Boss)---") + print("--- 关卡节点图·生成图推进(参数档×种子产图·满配通关/恢复公式/败北线/回放确定性)---") var autoplay = load("res://sim/level_autoplay.gd") var hero_base := {"atk": int(_db.hero.atk), "hp": int(_db.hero.max_hp), "ac": int(_db.hero.armor_class)} # 配置基线快照——⑤ 回基线对比用(改表自动跟随,不再硬编码) + var nodes1: Array = _level_active_nodes(1) + var params1: Dictionary = _db.level_gen_params_row(1) + Kit.check("L1 现行起关图取数非空(种子 %d × 参数档 → 节点 %d 颗)" % [ACTIVE_GRAPH_SEED, nodes1.size()], not nodes1.is_empty()) # ① 满配通关路径(atk+200%/hp+100%——测试面板同源的属性覆盖) - var win = autoplay.run_level(_db, 42, 1, 200, 100) + var win = autoplay.run_level(_db, ACTIVE_GRAPH_SEED, 1, 200, 100) Kit.check("关卡·满配(atk+200/hp+100)通关(实测:%s @节点 %d)" % [win.result, win.defeat_node_index], win.result == "victory") - Kit.check("战斗节点数 = 5(战×4 + Boss)", win.battles == 5) - Kit.check("Boss 节点以 Dmax=4 解析(棋盘生成 第三轴登记约定)", win.boss_depth == 4) + # 行进面读数与图读数自洽:路径逐跳为图出边,路径上战斗/Boss 计数 = battles 读数 + var by_id1 := _nodes_by_id(nodes1) + var path_battles := 0 + for vid in win.visited_ids: + var vkind := String(by_id1.get(int(vid), {}).get("type", "")) + if vkind == "battle" or vkind == "boss": + path_battles += 1 + Kit.check("L1 通关路径逐跳为图出边(入口起步·末节点无出边——句柄轴行进自洽)", _path_in_graph(nodes1, win.visited_ids)) + Kit.check("L1 通关路径战斗/Boss 节点数 = 行进 battles 读数(路径 %d 节点·战斗 %d 颗·battles %d)" % [win.visited_ids.size(), path_battles, int(win.battles)], + path_battles == int(win.battles) and path_battles >= 2) + # 终点恒落最长路径步数:Boss 深度 = 参数档层高,且通关路径末节点即该终点 + var boss_depth1 := -1 + for n in nodes1: + if String(n.get("type", "")) == "boss": + boss_depth1 = maxi(boss_depth1, int(n.get("depth", -1))) + var last_id := -1 + if win.visited_ids.size() > 0: + last_id = int(win.visited_ids[win.visited_ids.size() - 1]) + Kit.check("L1 终点 Boss 深度 = 参数档层高 %d 且通关路径末节点落该深度(实测终点深度 %d·末句柄 %d)" % [int(params1.get("layers", -1)), boss_depth1, last_id], + boss_depth1 == int(params1.get("layers", -1)) and int(by_id1.get(last_id, {}).get("depth", -1)) == boss_depth1) + Kit.check("L1 终点恰一个(大岔口自 L2 起的图侧投影)", _terminal_ids(nodes1).size() == 1 and _ids_of_type(nodes1, "boss").size() == 1) + Kit.check("L1 生成图含岔口(出度 ≥2 节点 %d 处)" % _fork_ids(nodes1).size(), _fork_ids(nodes1).size() >= 1) Kit.check("进 Boss HP 携带在域内(%d)" % win.hp_at_boss, win.hp_at_boss >= 1 and win.hp_at_boss <= int(_db.hero.max_hp) * 3) - # ② 恢复点:回复 = min(floor(生效max_hp×pct/100), 缺口)——hp+100% 档生效 max = 基线×2 - # seed 42(探针目的 = 验回复公式,种子仅载体) - var rest = autoplay.run_level(_db, 42, 1, 100, 100) - if rest.result != "start_failed" and rest.hp_before_rest >= 0: - var eff_max := maxi(1, int(round(float(_db.hero.max_hp) * 2.0))) # hp_pct=100(run_level 已恢复基线) - var expect: int = mini(int(floor(float(eff_max) * 0.3)), eff_max - rest.hp_before_rest) - Kit.check("恢复点回复 = floor(生效max_hp×30%%) 与缺口的较小值(%d/%d,生效 max %d)" % [rest.rest_healed, expect, eff_max], rest.rest_healed == expect) + # ② 恢复点公式:回复 = min(floor(生效max_hp×rest_heal_pct/100), 缺口)——两档缺口各验一条分支 + #(真入口驱动:起关→首战→选恢复候选;缺口由夹具造,恢复比例读关卡行) + var pct1: int = int(_db.level_row(1).get("rest_heal_pct", 0)) + var rest_wide := _drive_to_rest(1, ACTIVE_GRAPH_SEED, 100000) + if rest_wide.is_empty(): + Kit.check("恢复点公式无源(现行图入口后一位无可直达的恢复候选)", false) else: - Kit.check("恢复点未到达(seed 42 hp+100 在恢复点前败北?%s)" % rest.result, false) + var expect_pct := int(floor(float(int(rest_wide["max"])) * float(pct1) / 100.0)) + Kit.check("恢复点公式·缺口充分档:回复 = floor(生效max_hp×%d%%) = %d(生效 max %d,实测 %d)" % [pct1, expect_pct, int(rest_wide["max"]), int(rest_wide["healed"])], + int(rest_wide["healed"]) == expect_pct and expect_pct > 0) + var rest_narrow := _drive_to_rest(1, ACTIVE_GRAPH_SEED, 2) + if rest_narrow.is_empty(): + Kit.check("恢复点公式缺口封顶档无源(同图二次驱动未达恢复候选)", false) + else: + Kit.check("恢复点公式·缺口不足档:回复封顶到缺口 2(实测 %d,比例项 %d)" % [int(rest_narrow["healed"]), expect_pct], + int(rest_narrow["healed"]) == mini(expect_pct, 2) and expect_pct > 2) # ③ 裸装败北路径(atk1/hp10——Roguelike 基线应难以通关) - var lose = autoplay.run_level(_db, 42, 1, -80, -80) + var lose = autoplay.run_level(_db, ACTIVE_GRAPH_SEED, 1, -80, -80) Kit.check("关卡·裸装(atk≈1/hp≈10)败北路径可达(实测:%s)" % lose.result, lose.result == "defeat") - Kit.check("败北节点记录在域内(节点 %d)" % lose.defeat_node_index, lose.defeat_node_index >= 0 and lose.defeat_node_index < 7) + Kit.check("败北节点记录在域内(节点 %d,图节点数 %d)" % [lose.defeat_node_index, nodes1.size()], + lose.defeat_node_index >= 0 and lose.defeat_node_index < nodes1.size()) # ④ 回放确定性:同种子两遍步骤流逐行相同 - var rep1 = autoplay.run_level(_db, 777, 1, 100, 100, true, null, "normal", "min", 0) - var rep2 = autoplay.run_level(_db, 777, 1, 100, 100, true, null, "normal", "min", 0) + var rep1 = autoplay.run_level(_db, 777, 1, 100, 100, true, null, "normal", "min") + var rep2 = autoplay.run_level(_db, 777, 1, 100, 100, true, null, "normal", "min") Kit.check("关卡回放断言:同种子两遍结算步骤流逐行相同", JSON.stringify(rep1.rows) == JSON.stringify(rep2.rows)) Kit.check("关卡步骤流非空(%d 行)" % rep1.rows.size(), rep1.rows.size() > 0) # ⑤ 属性覆盖恢复:模拟后 hero 表回基线(调整板不泄漏到后续会话;基线 = 函数入口的配置快照,改表自动跟随) @@ -82,87 +378,130 @@ func _battle_st(policy: String) -> Dictionary: return {"result": "", "rounds": 0, "refreshes": 0, "mine_hits": 0, "hero_dmg": 0, "policy": policy} +# 真入口驱动到恢复节点并造缺口(起关→打赢入口战斗→在候选里取恢复型句柄→显式选路进点)。 +# 缺口 = 生效上限 − gap(gap 大于上限时按下限 1 血起算);返回 {healed, hp_before, gap, max}。 +# 现行图入口后一位无恢复候选时返回空字典(由调用方判空报红,禁静默跳过)。 +func _drive_to_rest(level_index: int, seed_val: int, gap: int) -> Dictionary: + var autoplay = load("res://sim/level_autoplay.gd") + var runner = load("res://core/level_runner.gd").new(_db, _fresh_rng(seed_val)) + runner.set_hero_override(200, 100) + if not runner.start_level(level_index, seed_val): + return {} + var first = runner.current_battle() + if first == null: + return {} + var st = _battle_st("normal") + var guard := 30 + while runner.current_battle() == first and guard > 0: + guard -= 1 + autoplay._autoplay_battle(runner, first, st, null) + if not runner.awaiting_choice(): + return {} + var by_id := _nodes_by_id(runner.nodes()) + var rest_id := -1 + for cid in runner.next_candidate_ids(): + if String(by_id.get(int(cid), {}).get("type", "")) == "rest": + rest_id = int(cid) + break + if rest_id < 0: + return {} + var eff_max: int = runner.effective_max_hp() + runner.hero_hp = maxi(1, eff_max - gap) + var hp_before: int = runner.hero_hp + if not runner.pick_next_node(rest_id): + return {} + return {"healed": runner.hero_hp - hp_before, "hp_before": hp_before, "gap": eff_max - hp_before, "max": eff_max} + + # ==================== 敌人技能池(开轮摇号出招系统) ==================== # 六项验收断言(proposal-skill-pool.md〔提案已吸收退役 git a6833b6a——现行事实源=敌人系统.md 技能池节〕 §8 批 1):同种子回放逐位复现 / 单敌轨迹独立复放 / # EV 抽样对锚 / 死亡打断作废 / 空池不可能 / 加载校验拒绝。 -# ---- 分支节点图冒烟:图合法性校验 + L2 幽暗地域图版行进(关卡节点地图 v1.0 图版)---- +# ---- 分支节点图冒烟:图合法性校验 + L2 幽暗地域生成图行进(图 = 参数档 × 种子)---- func _smoke_level_graph() -> void: - print("--- 关卡节点地图·分支节点图(L2 幽暗地域)---") + print("--- 关卡节点图·分支节点图(L2 幽暗地域·生成图)---") var autoplay = load("res://sim/level_autoplay.gd") - # ① L1 单链与 L2 图经同一套加载校验(单链 = 出边恰一的退化形态——load_tables 全绿已含,显式复述) - Kit.check("LevelTable 图合法性校验:L1 单链 + L2 图同套算法零错误", _db.level.validate().is_empty()) - # ② L2 行集结构(网状图建议规模) - var l2 := {} - for row in _db.level.rows: - if int(row.level_index) == 2: - l2 = row - Kit.check("L2 节点数 = 10(入口 1 + 中间三层 2/3/2 + 商店房 + Boss——商店房批 Boss 重编号 8→9)", not l2.is_empty() and _level_active_nodes(2).size() == 10) - var tcount := {"battle": 0, "reward": 0, "rest": 0, "boss": 0, "shop": 0} - var branch := 0 - for node in _level_active_nodes(2): - tcount[String(node.get("type", ""))] = int(tcount[String(node.get("type", ""))]) + 1 - if node.get("next", []).size() >= 2: - branch += 1 - Kit.check("L2 普通战斗节点 = 6 + Boss 恒一(编号最大恒终点)", int(tcount.battle) == 6 and int(tcount.boss) == 1) - Kit.check("L2 恢复节点 1(rest_heal_pct 行字段)+ 奖励节点 1(直通空态)+ 商店房节点 1(商店房批)", int(tcount.reward) == 1 and int(tcount.rest) == 1 and int(tcount.shop) == 1) - Kit.check("L2 真实岔路 = %d 处(≥2)" % branch, branch >= 2) - # ③ 图校验正反断言(违规行集定位到类别——加载期致命的同款算法) - var lt = load("res://data/schemas/level_table.gd").new() - var good := [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [2]}, - {"node_depth": 2, "type": "boss"}] - lt.rows.assign([{"level_index": 9, "display_name": "t", "rest_heal_pct": 30, "nodes": good}]) - Kit.check("图校验正例:两岔合流小图零错误", lt.validate().is_empty()) + # ① 关卡表行级校验零错误(关卡表只承载序号/显示名/恢复比例——图合法性由 validate_graph 在起关时承担) + Kit.check("LevelTable 行级校验零错误(图合法性移交 validate_graph)", _db.level.validate().is_empty()) + # ② L2 生成图形状读数(性质断言——规模/型别/岔口全部由参数档与逐节点扫描互校) + var nodes2: Array = _level_active_nodes(2) + var params2: Dictionary = _db.level_gen_params_row(2) + Kit.check("L2 现行起关图取数非空(种子 %d × 参数档 → 节点 %d 颗)" % [ACTIVE_GRAPH_SEED, nodes2.size()], not nodes2.is_empty()) + var tcount := _type_tally(nodes2) + var branch := _fork_ids(nodes2).size() + Kit.check("L2 节点数 %d ≥ 层高 %d+1 且 = 句柄去重读数 %d(全员句柄唯一)" % [nodes2.size(), int(params2.get("layers", -1)), _nodes_by_id(nodes2).size()], + nodes2.size() >= int(params2.get("layers", -1)) + 1 and _nodes_by_id(nodes2).size() == nodes2.size()) + Kit.check("L2 五型计数合计 = 节点数(battle %d + boss %d + reward %d + rest %d + shop %d = %d)" % [tcount["battle"], tcount["boss"], tcount["reward"], tcount["rest"], tcount["shop"], nodes2.size()], + int(tcount["battle"]) + int(tcount["boss"]) + int(tcount["reward"]) + int(tcount["rest"]) + int(tcount["shop"]) == nodes2.size()) + Kit.check("L2 战斗节点数 %d ≥ 参数档战斗保底 %d 且 Boss 节点数 %d = 参数档出口数 %d" % [tcount["battle"], int(params2.get("battle_floor", -1)), tcount["boss"], int(params2.get("exit_count", -1))], + int(tcount["battle"]) >= int(params2.get("battle_floor", -1)) and int(tcount["boss"]) == int(params2.get("exit_count", -1))) + Kit.check("L2 恢复/奖励/商店节点各 ≥1(实测 rest %d · reward %d · shop %d——计数由逐节点扫描同一读数)" % [tcount["rest"], tcount["reward"], tcount["shop"]], + int(tcount["rest"]) >= 1 and int(tcount["reward"]) >= 1 and int(tcount["shop"]) >= 1) + Kit.check("L2 岔口(出度 ≥2 的节点)= %d 处(≥1)" % branch, branch >= 1) + # ③ 图校验正反断言改走 validate_graph 公有面(迭代 35 卡② T1 随动·声明): + # · 夹具字段随生成图读面换代(id/type/depth/next 取代 node_depth 双编号空间); + # · 「入口唯一违例」判据随模板池退役——多入口是现行合法形态(关卡节点图生成 v1.1 结构铁律), + # 该条改判正例;「孤儿节点」条去掉「双入口同体违例」措辞,只保留到不了终点/进不去的违例面; + # · 前进方向判据由「目标编号严格递增」换为「目标深度严格大于源」(契约 1④)。 + var LT = load("res://data/schemas/level_table.gd") + var good := [{"id": 0, "type": "battle", "depth": 0, "next": [1, 2]}, + {"id": 1, "type": "battle", "depth": 1, "next": [2]}, + {"id": 2, "type": "boss", "depth": 2, "next": []}] + Kit.check("图校验正例:两岔合流小图零错误〔v1〕", LT.validate_graph(good).is_empty()) var dup = JSON.parse_string(JSON.stringify(good)) - dup[1].node_depth = 0 - lt.rows.assign([{"level_index": 9, "display_name": "t", "rest_heal_pct": 30, "nodes": dup}]) - Kit.check("图校验拒绝:节点编号重复(关卡内唯一定位地址)", _has_err_category(lt.validate(), "dup_key")) + dup[1].id = 0 + Kit.check("图校验拒绝:句柄重复(图内唯一句柄)〔v1〕", _has_err_category(LT.validate_graph(dup), "dup_key")) var dual = JSON.parse_string(JSON.stringify(good)) - dual.append({"node_depth": 9, "type": "battle", "next": [2]}) - lt.rows.assign([{"level_index": 9, "display_name": "t", "rest_heal_pct": 30, "nodes": dual}]) - Kit.check("图校验拒绝:第二无入边节点(入口唯一违例——入口节点数 = 3)", _has_err_category(lt.validate(), "structural")) + dual.append({"id": 9, "type": "battle", "depth": 0, "next": [2]}) + Kit.check("图校验正例:多入口合法(入口唯一判据随模板池退役)〔v1〕", LT.validate_graph(dual).is_empty()) var boss_out = JSON.parse_string(JSON.stringify(good)) boss_out[2].next = [1] - lt.rows.assign([{"level_index": 9, "display_name": "t", "rest_heal_pct": 30, "nodes": boss_out}]) - Kit.check("图校验拒绝:Boss 带出边(终点不可外出)", _has_err_category(lt.validate(), "structural")) + Kit.check("图校验拒绝:Boss 带出边(终点不可外出)〔v1〕", _has_err_category(LT.validate_graph(boss_out), "structural")) var cyc = JSON.parse_string(JSON.stringify(good)) cyc[1].next = [0] - lt.rows.assign([{"level_index": 9, "display_name": "t", "rest_heal_pct": 30, "nodes": cyc}]) - Kit.check("图校验拒绝:回边 1→0(连线只朝前进方向 ⇒ 无环)", _has_err_category(lt.validate(), "structural")) + Kit.check("图校验拒绝:回边 1→0(目标深度须严格大于源)〔v1〕", _has_err_category(LT.validate_graph(cyc), "structural")) var dang = JSON.parse_string(JSON.stringify(good)) dang[0].next = [1, 7] - lt.rows.assign([{"level_index": 9, "display_name": "t", "rest_heal_pct": 30, "nodes": dang}]) - Kit.check("图校验拒绝:出边指向不存在编号(悬空致命)", _has_err_category(lt.validate(), "dangling_next")) + Kit.check("图校验拒绝:出边指向不存在句柄(悬空致命)〔v1〕", _has_err_category(LT.validate_graph(dang), "dangling_next")) var orphan = JSON.parse_string(JSON.stringify(good)) orphan[0].next = [1] orphan[1].next = [2] - orphan.append({"node_depth": 5, "type": "battle", "next": [2]}) - lt.rows.assign([{"level_index": 9, "display_name": "t", "rest_heal_pct": 30, "nodes": orphan}]) - var orphan_errs: Array = lt.validate() - Kit.check("图校验拒绝:孤儿节点(从入口不可达 / 双入口同体违例)", _has_err_category(orphan_errs, "structural")) - # ④ L2 选路推进:min / max 双路径全通(确定性路径轴——零随机流自开) - # 战斗随机值面随全局序漂移——max 路径种子按行为面扫描(路线形状/商店/选路口为断言本体) - var win_min = autoplay.run_level(_db, 42, 2, 200, 100, false, null, "normal", "min", 0) + orphan.append({"id": 5, "type": "battle", "depth": 1, "next": [6]}) + orphan.append({"id": 6, "type": "rest", "depth": 2, "next": []}) + Kit.check("图校验拒绝:非 Boss 终点(终点恒为 Boss)〔v1〕", _has_err_category(LT.validate_graph(orphan), "structural")) + # ④ L2 选路推进:min / max 双路径全通(确定性路径轴——同种子同图,方向轴只换候选取值) + var win_min: Dictionary = autoplay.run_level(_db, ACTIVE_GRAPH_SEED, 2, 200, 100, false, null, "normal", "min") Kit.check("L2·满配 min 路径通关(实测:%s)" % win_min.result, win_min.result == "victory") - Kit.check("L2 Boss 以编号 9 解析(终点编号最大——商店房批重编号 8→9)", win_min.boss_depth == 9) - Kit.check("min 路径 = 0→1→3(休整)→7→8(商店零购离开)→9(实测 %s——T17 出度≤2 拍板模板 0 重建)" % str(win_min.visited_ids), win_min.visited_ids == [0, 1, 3, 7, 8, 9]) + var n2_last := -1 + if win_min.visited_ids.size() > 0: + n2_last = int(win_min.visited_ids[win_min.visited_ids.size() - 1]) + var n2_last_type := String(_nodes_by_id(nodes2).get(n2_last, {}).get("type", "")) + Kit.check("L2 min 路径逐跳为图出边·末节点为 Boss(实测路径 %s·末句柄型别 %s)" % [str(win_min.visited_ids), n2_last_type], + _path_in_graph(nodes2, win_min.visited_ids) and n2_last_type == "boss") + Kit.check("L2 min 路径终点深度 = 参数档层高 %d(行进读数 %d——终点落最长路径步数)" % [int(params2.get("layers", -1)), int(win_min.boss_depth)], + int(win_min.boss_depth) == int(params2.get("layers", -1))) + Kit.check("L2 min 路径穿商店恰 1 家(路径句柄 %s 型别计数 = 1——必经恰一店的行进面)" % str(win_min.visited_ids), + _path_shop_count(nodes2, win_min.visited_ids) == 1) Kit.check("选路口发生 %d 次(每节点兑付后——零自动推进)" % win_min.choices, win_min.choices >= 3) - # max 路径 = 存储种子(T17 改造——废除逐种子扫描):本项目核心承诺=确定性复放(同代码同表 - # 同种子逐位一致),「能赢且路径符合的种子」是恒定事实,采一次存死即可。旧形态 range(42,300) - # 逐种子全关卡通关扫描找命中,期望路径过期时 258 次模拟撞 180s 闸(台账 257 + - # 出度≤2 重建两次实证)。存储种子常态一次跑完;图表/数值有意变更致本断言红=快速失败(一次 - # 即红非扫描超时),重采=临时脚本跑同款 run_level 循环取首命中种子回填(离线一次性)。 - const MAX_PATH_SEED := 43 # 种子重采程序:同款循环首命中(断言注释既定程序,临时脚本用后即删) - var win_max: Dictionary = autoplay.run_level(_db, MAX_PATH_SEED, 2, 200, 100, false, null, "normal", "max", 0) - Kit.check("L2·满配 max 路径(max 取最大编号候选)通关 = 0→2(奖励)→6→7→8(商店零购离开)→9(存储种子 %d——T17 出度≤2 拍板模板 0 重建)" % MAX_PATH_SEED, win_max.result == "victory" and win_max.visited_ids == [0, 2, 6, 7, 8, 9]) - # ⑤ 零自动推进状态机(手动驱动——兑付确认 / 选路拒绝语义逐拍断言) - var runner = load("res://core/level_runner.gd").new(_db, _fresh_rng(42)) + var win_max: Dictionary = autoplay.run_level(_db, ACTIVE_GRAPH_SEED, 2, 200, 100, false, null, "normal", "max") + Kit.check("L2·满配 max 路径(候选取最大句柄)通关(实测:%s·路径 %s)" % [win_max.result, str(win_max.visited_ids)], win_max.result == "victory") + var n2_last_max := -1 + if win_max.visited_ids.size() > 0: + n2_last_max = int(win_max.visited_ids[win_max.visited_ids.size() - 1]) + var n2_last_max_type := String(_nodes_by_id(nodes2).get(n2_last_max, {}).get("type", "")) + Kit.check("L2 max 路径逐跳为图出边·末节点为 Boss·穿商店恰 1 家", + _path_in_graph(nodes2, win_max.visited_ids) and n2_last_max_type == "boss" + and _path_shop_count(nodes2, win_max.visited_ids) == 1) + Kit.check("L2 max 与 min 路径分岔(方向轴生效——两路终点句柄 %d / %d 不同)" % [n2_last, n2_last_max], + n2_last != n2_last_max and win_max.visited_ids != win_min.visited_ids) + # ⑤ 零自动推进状态机(手动驱动——兑付确认 / 选路拒绝语义逐拍断言,候选定位全部按图型别数据驱动) + var runner = load("res://core/level_runner.gd").new(_db, _fresh_rng(ACTIVE_GRAPH_SEED)) runner.set_hero_override(200, 100) var choice_sizes: Array = [] runner.choices_presented.connect(func(c): choice_sizes.append(c.size())) - Kit.check("手动驱动:L2 启动(入口战斗直接开打——入口无前置兑付;template_pick=0 固定现行图——精确选路断言)", runner.start_level(2, 42, {}, 0)) + Kit.check("手动驱动:L2 启动(入口战斗直接开打——入口无前置兑付;图 = 参数档 × 种子 %d)" % ACTIVE_GRAPH_SEED, runner.start_level(2, ACTIVE_GRAPH_SEED)) var st = _battle_st("normal") var first = runner.current_battle() var res_box := {} # 信号写回容器(lambda 按值捕获局部变量——Dictionary 引用可变) @@ -170,12 +509,46 @@ func _smoke_level_graph() -> void: while runner.current_battle() == first and not res_box.has("result"): autoplay._autoplay_battle(runner, first, st, null) Kit.check("首战胜利后:settlement 释放 + 进选路态(战斗胜利不再自动推进——接口漂移清偿)", String(res_box.get("result", "")) == "victory" and runner.current_battle() == null and runner.awaiting_choice()) - Kit.check("入口岔路候选 = [1, 2](T17 出度≤2 拍板模板 0 宽廊双干——层宽 4 上限随出度约束)", runner.next_candidate_ids() == [1, 2]) - Kit.check("候选外编号拒绝且仍待选(拒绝语义)", not runner.pick_next_node(8) and runner.awaiting_choice()) - Kit.check("选编号 2(奖励点)受理", runner.pick_next_node(2)) + # 消费面图与生成面逐位一致(runner 持图 = 同一参数档同一种子的产图——读面句柄集相同) + var rnodes: Array = runner.nodes() + Kit.check("runner 消费面图 = 生成面图(节点数 %d/%d 且句柄集逐位相同)" % [rnodes.size(), nodes2.size()], + rnodes.size() == nodes2.size() and _ids_of_type(rnodes, "battle").size() + _ids_of_type(rnodes, "boss").size() + _ids_of_type(rnodes, "reward").size() \ + + _ids_of_type(rnodes, "rest").size() + _ids_of_type(rnodes, "shop").size() == nodes2.size() and _nodes_by_id(rnodes) == _nodes_by_id(nodes2)) + var cand: Array = runner.next_candidate_ids() + var rbyid := _nodes_by_id(rnodes) + var cand_types: Array = [] + for cid in cand: + cand_types.append(String(rbyid.get(int(cid), {}).get("type", "<不在图>"))) + Kit.check("入口选路口候选非空且全在图内(候选 %s·类型 %s)" % [str(cand), str(cand_types)], not cand.is_empty() and not cand_types.has("<不在图>")) + Kit.check("候选下一跳类型两两互异(岔口去路类型不重复契约——实测 %s)" % str(cand_types), cand_types.size() == _uniq(cand_types).size()) + # 候选外句柄拒绝(取图内非候选的句柄——数据驱动定位) + var outside_id := -1 + for n in rnodes: + if not cand.has(int(n.get("id", -1))) and int(n.get("id", -1)) != int(runner.current_node_id()): + outside_id = int(n.get("id", -1)) + break + Kit.check("候选外句柄 %d 拒绝且仍待选(拒绝语义)" % outside_id, outside_id >= 0 and not runner.pick_next_node(outside_id) and runner.awaiting_choice()) + # 奖励分支点选(候选按型别定位——奖励位存在才断言,缺位即红不静默跳过) + var reward_cand := -1 + var battle_cand := -1 + for cid in cand: + var ct := String(rbyid.get(int(cid), {}).get("type", "")) + if ct == "reward" and reward_cand < 0: + reward_cand = int(cid) + if ct == "battle" and battle_cand < 0: + battle_cand = int(cid) + Kit.check("入口岔口含奖励位与战斗位(句柄 %d/%d——候选型别可定位)" % [reward_cand, battle_cand], reward_cand >= 0 and battle_cand >= 0) + Kit.check("选奖励候选 %d 受理" % reward_cand, runner.pick_next_node(reward_cand)) Kit.check("到达奖励点:直通兑付(确认前不出选路口)", String(runner.current_node().get("type", "")) == "reward" and not runner.awaiting_choice()) - Kit.check("兑付确认后才出现选路口(continue_after_node 泛化语义)候选 = [5, 6]", runner.continue_after_node() and runner.awaiting_choice() and runner.next_candidate_ids() == [5, 6]) - Kit.check("选编号 6(巨蜘蛛巢群)开战", runner.pick_next_node(6) and runner.current_battle() != null) + var reward_outs: Array = runner.node_out_ids(runner.current_node()) + Kit.check("兑付确认后才出现选路口(continue_after_node 泛化语义)候选 = 该点图出边 %s" % str(reward_outs), + runner.continue_after_node() and runner.awaiting_choice() and _sorted_ids(runner.next_candidate_ids()) == _sorted_ids(reward_outs)) + var next_battle := -1 + for cid in runner.next_candidate_ids(): + if String(rbyid.get(int(cid), {}).get("type", "")) == "battle": + next_battle = int(cid) + break + Kit.check("选下一跳战斗候选 %d 开战" % next_battle, next_battle >= 0 and runner.pick_next_node(next_battle) and runner.current_battle() != null) # ⑥ L2 锚行首遇战:基线 0 加成 · 完美档(零盲挖 = 零踩雷上限口径)多种子扫描 # (绿档口径 = 12 局多数胜 + 零踩雷恒要求;还原全胜档需回调锚行血量,归用户决策) var green_ok := true @@ -203,24 +576,14 @@ func _smoke_level_graph() -> void: Kit.check("首遇位零踩雷:完美档 12 种子(节点 0 熊地精)恒零踩雷", green_ok) Kit.check("首遇战胜局 ≥7/12(实际 %d——阶段拔高后锚行退出绿档,多数胜口径)" % green_wins, green_wins >= 7) Kit.check("首遇战胜后 HP 下探读数 %d(>0——绿档记录)" % green_min_hp, green_min_hp > 0) - # ⑦ 跨表行完整:L2 每战斗/Boss 节点在 board_gen 与 enemy 精确行集(加载校验同款显式复述; - # enemy 键域 = (level_index, row_kind, depth_band)——战斗节点查 battle 键域、Boss 节点查 boss 键域) - var bg_keys := {} - for row in _db.board_gen.rows: - bg_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true - var en_keys := {} - for row in _db.enemy.rows: - en_keys["%d_%s_%d" % [int(row.level_index), String(row.get("row_kind", "")), int(row.get("depth_band", -1))]] = true - var cross_ok := true - for node in _level_active_nodes(2): - var t := String(node.get("type", "")) - if t != "battle" and t != "boss": - continue - if not bg_keys.has("2_%d" % int(node.node_depth)) or not en_keys.has("2_%s_%d" % [t, int(node.node_depth)]): - cross_ok = false - Kit.check("跨表行完整:L2 全部战斗/Boss 节点在 board_gen + enemy 精确行集(7 节点——带键 (2,row_kind,depth_band))", cross_ok) - # ⑦a 定点:L2 首遇带行集与 Boss 行集(首遇带 = 数据驱动最低带,契约 8——禁写死坐标; - # 断言行集构成而非单行:行序 = 文件序,首行即叙述取数面) + # ⑦ 跨表覆盖义务:L2 现行起关图每战斗/Boss 节点深度的敌侧落带 ≥ 请求深度(顶 clamp 消解即红) + # +每深度 board_gen 精确命中(判据实现与〔v11〕种子扫描段同源——_enemy_band_covers/_board_row_exact 单点) + var cov2 := _coverage_scan(2, nodes2) + Kit.check("跨表覆盖扫描面非空:L2 现行图战斗/Boss 节点 %d 颗" % int(cov2["nodes"]), int(cov2["nodes"]) >= 1) + Kit.check("L2 现行图每战斗/Boss 节点深度敌侧解析落带 ≥ 请求深度(违例 %d 处)" % int(cov2["enemy_bad"]), int(cov2["enemy_bad"]) == 0) + Kit.check("L2 现行图每节点深度 board_gen 精确命中(非空且 node_depth 等值·违例 %d 处)" % int(cov2["board_bad"]), int(cov2["board_bad"]) == 0) + # ⑦a 定点:L2 首遇带行集与各终点 Boss 行集(首遇带 = 数据驱动最低带,契约 8——禁写死坐标; + # 多终点图逐终点解析,断言行集构成而非单行:行序 = 文件序,首行即叙述取数面) var l2_spider_band: int = _first_encounter_band(2, "giant_spider") var l2_spider_set: Array = _db.resolve_enemy_rowset(2, l2_spider_band, "battle") var l2_spider_ids := {} @@ -229,11 +592,14 @@ func _smoke_level_graph() -> void: Kit.check("巨蜘蛛首遇带(2,battle,%d——扫表取最低带)行集 = {giant_spider} 恰一行且 unit_count=2 无 unit_count_max(固定 2 只——首遇教学位不加骰)〔fv22〕" % l2_spider_band, l2_spider_band >= 0 and l2_spider_ids.has("giant_spider") and l2_spider_ids.size() == 1 and l2_spider_set.size() == 1 \ and int(l2_spider_set[0].get("unit_count", -1)) == 2 and not l2_spider_set[0].has("unit_count_max")) - var l2_boss_set: Array = _db.resolve_enemy_rowset(2, _boss_depth(2), "boss") - var l2_boss_ids := {} - for r in l2_boss_set: - l2_boss_ids[String(r.get("enemy_id", ""))] = true - Kit.check("蛛化卓尔精灵 Boss 行集(2,boss,%d)= {drider} 恰一行且首行魔法甲(护甲类型重配批 2026-09-18:关卡 2 唯一非轻甲;P4 乙 Boss 行集多行合法——零内容期仍单行)〔fv24〕" % _boss_depth(2), l2_boss_ids.has("drider") and l2_boss_ids.size() == 1 and String(l2_boss_set[0].get("armor_type", "")) == "ward") + var l2_terms := _terminal_depths(nodes2) + var l2_boss_ok := not l2_terms.is_empty() + for d in l2_terms: + var l2_boss_set: Array[Dictionary] = _db.resolve_enemy_rowset(2, int(d), "boss") + if l2_boss_set.size() != 1 or String(l2_boss_set[0].get("enemy_id", "")) != "drider" \ + or String(l2_boss_set[0].get("armor_type", "")) != "ward": + l2_boss_ok = false + Kit.check("L2 全部终点深度 %s 的 Boss 行集均 = {drider} 恰一行且首行魔法甲(护甲类型重配批:关卡 2 唯一非轻甲;P4 乙 Boss 行集多行合法——零内容期仍单行)〔fv24〕" % str(l2_terms), l2_boss_ok) # ⑦b 定点:混编遭遇行集 (2,battle,5)(R6 行序契约运行面: # 行集两行文件内相邻、行序 = 声明序 = 生成序;静态最坏骰和 Σ(unit_count_max,固定行缺省 = unit_count) ≤ 4) var l2_mix_set: Array = _db.resolve_enemy_rowset(2, 5, "battle") @@ -249,32 +615,29 @@ func _smoke_level_graph() -> void: int(l2_mix_set[1].get("unit_count", 0)) == 2 and int(l2_mix_set[1].get("unit_count_max", 0)) == 3 \ and int(l2_mix_set[1].get("hp", 0)) == 26 and int(l2_mix_set[1].get("armor_class", -1)) == 2) # ---- L3 灰矮人矿城断言段 ---- - # ⑧ L3 行集结构(复用 L2 九节点图模板——6 战 + Boss + 奖励 + 恢复,真实岔路 4 处) - var l3 := {} - for row in _db.level.rows: - if int(row.level_index) == 3: - l3 = row - Kit.check("L3 节点数 = 10(入口 1 + 中间三层 2/3/2 + 商店房 + Boss——L2 同模板 + 商店房批)", not l3.is_empty() and _level_active_nodes(3).size() == 10) - var tcount3 := {"battle": 0, "reward": 0, "rest": 0, "boss": 0, "shop": 0} - var branch3 := 0 - for node in _level_active_nodes(3): - tcount3[String(node.get("type", ""))] = int(tcount3[String(node.get("type", ""))]) + 1 - if node.get("next", []).size() >= 2: - branch3 += 1 - Kit.check("L3 普通战斗节点 = 6 + Boss 恒一(编号最大恒终点)", int(tcount3.battle) == 6 and int(tcount3.boss) == 1) - Kit.check("L3 恢复节点 1(rest_heal_pct 行字段)+ 奖励节点 1(直通空态)+ 商店房节点 1(商店房批)", int(tcount3.reward) == 1 and int(tcount3.rest) == 1 and int(tcount3.shop) == 1) - Kit.check("L3 真实岔路 = %d 处(0→[1,2,3,4,5] · 2→[6,7] · 5→[6,7]——U2 返工批五岔环廊层宽 5)" % branch3, branch3 == 3) - # ⑨ 跨表行完整:L3 每战斗/Boss 节点在 board_gen 与 enemy 精确行集(⑦ 的 bg_keys/en_keys 复用—— - # 键域同形:(L,row_kind,depth_band)) - var cross_ok3 := true - for node in _level_active_nodes(3): - var t3 := String(node.get("type", "")) - if t3 != "battle" and t3 != "boss": - continue - if not bg_keys.has("3_%d" % int(node.node_depth)) or not en_keys.has("3_%s_%d" % [t3, int(node.node_depth)]): - cross_ok3 = false - Kit.check("跨表行完整:L3 全部战斗/Boss 节点在 board_gen + enemy 精确行集(7 节点——带键 (3,row_kind,depth_band))", cross_ok3) - # ⑩ 定点:L3 首遇带行集与 Boss 行集(克制面重轻错开——重甲灰矮人首次登台;首遇带数据驱动定位同 ⑦a) + # ⑧ L3 生成图形状读数(与 ② 同一性质判据面——规模/型别/岔口/多终点全部对参数档与扫描读数互校) + var nodes3: Array = _level_active_nodes(3) + var params3: Dictionary = _db.level_gen_params_row(3) + Kit.check("L3 现行起关图取数非空(种子 %d × 参数档 → 节点 %d 颗)" % [ACTIVE_GRAPH_SEED, nodes3.size()], not nodes3.is_empty()) + var tcount3 := _type_tally(nodes3) + var branch3 := _fork_ids(nodes3).size() + Kit.check("L3 节点数 %d ≥ 层高 %d+1 且 = 句柄去重读数 %d(全员句柄唯一)" % [nodes3.size(), int(params3.get("layers", -1)), _nodes_by_id(nodes3).size()], + nodes3.size() >= int(params3.get("layers", -1)) + 1 and _nodes_by_id(nodes3).size() == nodes3.size()) + Kit.check("L3 五型计数合计 = 节点数(battle %d + boss %d + reward %d + rest %d + shop %d = %d)" % [tcount3["battle"], tcount3["boss"], tcount3["reward"], tcount3["rest"], tcount3["shop"], nodes3.size()], + int(tcount3["battle"]) + int(tcount3["boss"]) + int(tcount3["reward"]) + int(tcount3["rest"]) + int(tcount3["shop"]) == nodes3.size()) + Kit.check("L3 战斗节点数 %d ≥ 参数档战斗保底 %d 且 Boss 节点数 %d = 参数档出口数 %d" % [tcount3["battle"], int(params3.get("battle_floor", -1)), tcount3["boss"], int(params3.get("exit_count", -1))], + int(tcount3["battle"]) >= int(params3.get("battle_floor", -1)) and int(tcount3["boss"]) == int(params3.get("exit_count", -1))) + Kit.check("L3 恢复/奖励/商店节点各 ≥1(实测 rest %d · reward %d · shop %d——计数由逐节点扫描同一读数)" % [tcount3["rest"], tcount3["reward"], tcount3["shop"]], + int(tcount3["rest"]) >= 1 and int(tcount3["reward"]) >= 1 and int(tcount3["shop"]) >= 1) + Kit.check("L3 岔口(出度 ≥2 的节点)= %d 处(≥1)" % branch3, branch3 >= 1) + Kit.check("L3 入口数 %d = 参数档 entry_count · 终点数 %d = exit_count ≥2(多终点大岔口自 L2 起)" % [_entry_ids(nodes3).size(), _terminal_ids(nodes3).size()], + _entry_ids(nodes3).size() == int(params3.get("entry_count", -1)) and _terminal_ids(nodes3).size() == int(params3.get("exit_count", -1)) and _terminal_ids(nodes3).size() >= 2) + # ⑨ 跨表覆盖义务(同 ⑦ 判据面):L3 现行图每战斗/Boss 节点深度敌侧落带 ≥ 请求深度+板侧精确命中 + var cov3 := _coverage_scan(3, nodes3) + Kit.check("跨表覆盖扫描面非空:L3 现行图战斗/Boss 节点 %d 颗" % int(cov3["nodes"]), int(cov3["nodes"]) >= 1) + Kit.check("L3 现行图每战斗/Boss 节点深度敌侧解析落带 ≥ 请求深度(违例 %d 处)" % int(cov3["enemy_bad"]), int(cov3["enemy_bad"]) == 0) + Kit.check("L3 现行图每节点深度 board_gen 精确命中(非空且 node_depth 等值·违例 %d 处)" % int(cov3["board_bad"]), int(cov3["board_bad"]) == 0) + # ⑩ 定点:L3 首遇带行集与各终点 Boss 行集(克制面重轻错开——重甲灰矮人首次登台;首遇带数据驱动定位同 ⑦a) var l3_lead_band: int = _first_encounter_band(3, "duergar") var l3_lead_set: Array = _db.resolve_enemy_rowset(3, l3_lead_band, "battle") var l3_lead_ids := {} @@ -284,11 +647,14 @@ func _smoke_level_graph() -> void: l3_lead_band >= 0 and l3_lead_ids.has("duergar") and l3_lead_ids.size() == 1 \ and String(l3_lead_set[0].get("armor_type", "")) == "heavy" \ and int(l3_lead_set[0].get("unit_count", -1)) == 1 and not l3_lead_set[0].has("unit_count_max")) - var l3_king_set: Array = _db.resolve_enemy_rowset(3, _boss_depth(3), "boss") - var l3_king_ids := {} - for r in l3_king_set: - l3_king_ids[String(r.get("enemy_id", ""))] = true - Kit.check("灰矮人石君王 Boss 行集(3,boss,%d)= {duergar_king} 恰一行且首行重甲(重甲收束「轻甲→魔法甲→重甲」教学梯度)〔fv24〕" % _boss_depth(3), l3_king_ids.has("duergar_king") and l3_king_ids.size() == 1 and String(l3_king_set[0].get("armor_type", "")) == "heavy") + var l3_terms := _terminal_depths(nodes3) + var l3_boss_ok := not l3_terms.is_empty() + for d in l3_terms: + var l3_king_set: Array[Dictionary] = _db.resolve_enemy_rowset(3, int(d), "boss") + if l3_king_set.size() != 1 or String(l3_king_set[0].get("enemy_id", "")) != "duergar_king" \ + or String(l3_king_set[0].get("armor_type", "")) != "heavy": + l3_boss_ok = false + Kit.check("L3 全部终点深度 %s 的 Boss 行集均 = {duergar_king} 恰一行且首行重甲(重甲收束「轻甲→魔法甲→重甲」教学梯度)〔fv24〕" % str(l3_terms), l3_boss_ok) # ⑪ L3 成长代理档首遇绿档:模拟 GP 商店成长后的入关参数(atk_pct=200 / hp_pct=100)· 完美档多种子首遇战稳过 # (零加成基线红档属预期——断言口径 = 成长代理档,非基线;跑法同 ⑥) var green3_ok := true @@ -335,13 +701,13 @@ func _smoke_level_graph() -> void: _db.enemy.rows.remove_at(_db.enemy.rows.size() - 1) _db.enemy.rows.remove_at(_db.enemy.rows.size() - 1) _db.enemy.rows.remove_at(_db.enemy.rows.size() - 1) - Kit.check("混编断言段注入行全量还原(表回 20 行——首批混编投放后基线行数,迭代 18 卡⑤;后续断言面零污染)", int(_db.enemy.rows.size()) == 20) + Kit.check("混编断言段注入行全量还原(表回 28 行——迭代 35 生成图深度扩面覆盖批后的基线行数;首批混编投放基线 20 行+覆盖 8 行)", int(_db.enemy.rows.size()) == 28) # ⑬ 真图类型可见性(信息迷雾 v2.1):L2 入口停点纯核心面读数——候选 ⊆ 可见集 / - # Boss 恒可见 / 层距 > N 的非豁免节点入雾。N 取数据表(代码零硬编码)。 - var vis_runner = load("res://core/level_runner.gd").new(_db, _fresh_rng(42)) + # 终点恒可见 / 入口 BFS 层距 > N 的非豁免节点入雾。N 取数据表(代码零硬编码)。 + var vis_runner = load("res://core/level_runner.gd").new(_db, _fresh_rng(ACTIVE_GRAPH_SEED)) var vis_ledger := {"hero_hp": 10, "gp": 0} - var vis_ok: bool = vis_runner.start_level_external(2, vis_ledger, {}, Callable(), {}, true, 0) + var vis_ok: bool = vis_runner.start_level_external(2, vis_ledger, {}, Callable(), {}, true) Kit.check("可见性冒烟:L2 入口停点就位(defer_entry——无当前节点,锚层 0)", vis_ok and vis_runner.awaiting_choice() and vis_runner.current_node_id() == -1) var vis_ids: Array = vis_runner.type_visible_ids() var vis_cand_in := true @@ -349,10 +715,36 @@ func _smoke_level_graph() -> void: if not vis_ids.has(int(cid)): vis_cand_in = false Kit.check("可见性冒烟:入口停点候选集 ⊆ 类型可见集(下一跳恒可见)〔fv4〕", vis_cand_in) - Kit.check("可见性冒烟:L2 Boss(编号 %d)恒在可见集(终点不入雾)〔fv3〕" % _boss_depth(2), vis_ids.has(_boss_depth(2))) + var vis_boss_ok := true + for bid in _ids_of_type(nodes2, "boss"): + if not vis_ids.has(int(bid)): + vis_boss_ok = false + Kit.check("可见性冒烟:L2 全部终点 Boss 句柄 %s 恒在可见集(终点不入雾)〔fv3〕" % str(_ids_of_type(nodes2, "boss")), vis_boss_ok) var vis_n: int = int(_db.level.info_visible_layers) Kit.check("可见性冒烟:N 读数据表 = %d(provisional 2——代码零硬编码)〔fv6〕" % vis_n, vis_n == 2) - Kit.check("可见性冒烟:入口锚层 0 时层距 3/4 的节点 7/8 入雾(非豁免——入口 BFS 层差 > N)〔fv1〕", not vis_ids.has(7) and not vis_ids.has(8)) + # 层距判据按图自算入口 BFS 层(与 runner 同源口径:入口层 0、出边逐层 +1)—— + # 锚层 0 时层距 ≤ N 者恒可见、层距 > N 的非终点者恒不可见(两条扫描面都须非空) + var vis_layers := _bfs_layers(nodes2) + var near_ok := true + var near_count := 0 + var fog_ok := true + var fog_count := 0 + for n in nodes2: + var nid := int(n.get("id", -1)) + var layer: int = int(vis_layers.get(nid, -1)) + if layer < 0: + continue + var is_boss := String(n.get("type", "")) == "boss" + if layer <= vis_n: + near_count += 1 + if not vis_ids.has(nid): + near_ok = false + elif not is_boss: + fog_count += 1 + if vis_ids.has(nid): + fog_ok = false + Kit.check("可见性冒烟:入口锚层 0 时层距 ≤ %d 的节点全在可见集(扫描面 %d 颗·违例 0)〔fv1〕" % [vis_n, near_count], near_count >= 1 and near_ok) + Kit.check("可见性冒烟:入口锚层 0 时层距 > %d 的非终点节点全入雾(扫描面 %d 颗·违例 0)〔fv1〕" % [vis_n, fog_count], fog_count >= 1 and fog_ok) # 冒烟隔离用全新 RngService(同 master 种子——回放确定性纪律) @@ -367,7 +759,7 @@ func _fresh_rng(seed_val: int): # ==================== GP+商店房冒烟 ==================== # ① GP 到账(战斗完成入账恰一次 + Boss 关级加值)② escrow 隔离(格面兑付入缓冲/破财不透支) -# ③ 商店三收口(购买出账/加算区/余额不足拒/离开/收束清零)④ 图不变式(商店必经位/跨表行集迁移) +# ③ 商店三收口(购买出账/加算区/余额不足拒/离开/收束清零)④ 商店编排性质(每终点前恰一店/终点跨表解析) # ⑤ 九型与 gp_drop 流(奖励字典 18 格 + 黄金向量)。 # ---- ① GP 冒险钱包·到账 ---- @@ -375,7 +767,7 @@ func _fresh_rng(seed_val: int): # ==================== GP+商店房冒烟 ==================== # ① GP 到账(战斗完成入账恰一次 + Boss 关级加值)② escrow 隔离(格面兑付入缓冲/破财不透支) -# ③ 商店三收口(购买出账/加算区/余额不足拒/离开/收束清零)④ 图不变式(商店必经位/跨表行集迁移) +# ③ 商店三收口(购买出账/加算区/余额不足拒/离开/收束清零)④ 商店编排性质(每终点前恰一店/终点跨表解析) # ⑤ 九型与 gp_drop 流(奖励字典 18 格 + 黄金向量)。 # ---- ① GP 冒险钱包·到账 ---- @@ -561,44 +953,61 @@ func _find_gp_dig(minesweeper, settlement) -> Dictionary: func _smoke_shop_room() -> void: print("--- 商店房三收口(现价/加算区/余额不足拒/离开回选路/收束清零)---") var autoplay = load("res://sim/level_autoplay.gd") - # 行进到商店房(min 路径:0→1→3(恢复)→6→8)——种子扫描保证进店余额 ≥ 三件首购总价 16(6+4+6) + # 行进到商店房(按图型别定位商店节点——候选取最小句柄行进,到达即停): + # 种子扫描保证进店余额 ≥ 三件首购总价 16(6+4+6) var shop_run := {} for s in range(42, 92): - var runner = load("res://core/level_runner.gd").new(_db, _fresh_rng(s)) - runner.set_hero_override(900, 400) # 行进段快速通关档(购买断言与百分比轴正交——加算区单独断言) - runner.start_level(2, s, {}, 0) # 固定模板 0——商店经济断言按现行图 GP 账本 + var r0 = load("res://core/level_runner.gd").new(_db, _fresh_rng(s)) + r0.set_hero_override(900, 400) # 行进段快速通关档(购买断言与百分比轴正交——加算区单独断言) + r0.start_level(2, s) var st := _battle_st("normal") - var guard := 40 - while String(runner.level_result) == "" and int(runner.current_node_id()) != 8 and guard > 0: + var guard := 60 + while String(r0.level_result) == "" and String(r0.current_node().get("type", "")) != "shop" and guard > 0: guard -= 1 - if runner.awaiting_choice(): - var ids: Array = runner.next_candidate_ids() + if r0.awaiting_choice(): + var ids: Array = r0.next_candidate_ids() ids.sort() - runner.pick_next_node(int(ids[0])) + r0.pick_next_node(int(ids[0])) continue - var t := String(runner.current_node().get("type", "")) + var t := String(r0.current_node().get("type", "")) if t == "reward" or t == "rest": - runner.continue_after_node() + r0.continue_after_node() continue - if t == "shop": - break - var b = runner.current_battle() + var b = r0.current_battle() if b == null: break var fin := {} b.battle_finished.connect(func(p): fin["p"] = p) var g2 := 80 - while runner.current_battle() == b and not fin.has("p") and g2 > 0: + while r0.current_battle() == b and not fin.has("p") and g2 > 0: g2 -= 1 - autoplay._autoplay_battle(runner, b, st, null) - if String(runner.level_result) == "" and int(runner.current_node_id()) == 8 and runner.gp >= 16: - shop_run = {"seed": s, "runner": runner} + autoplay._autoplay_battle(r0, b, st, null) + if String(r0.level_result) == "" and String(r0.current_node().get("type", "")) == "shop" and r0.gp >= 16: + shop_run = {"seed": s, "runner": r0} break Kit.check("商店房到达种子可达(seed=%s——进店 GP ≥ 16)" % str(shop_run.get("seed", -1)), not shop_run.is_empty()) if shop_run.is_empty(): return var runner = shop_run["runner"] - Kit.check("L2 行进到商店房(节点 8,next=[9]——Boss 前必经位)", String(runner.current_node().get("type", "")) == "shop" and int(runner.current_node_id()) == 8) + # 到店读数全部按图解析(现行判据=每终点前一位恰一店的行进面) + var s_nodes: Array = runner.nodes() + var s_byid := _nodes_by_id(s_nodes) + var shop_id: int = int(runner.current_node_id()) + var shop_outs: Array = runner.node_out_ids(runner.current_node()) + var shop_preds := 0 + for n in s_nodes: + if (n.get("next", []) as Array).has(shop_id): + shop_preds += 1 + Kit.check("L2 行进到商店房:当前节点型别 = shop 且出边恰一条指终点 Boss(句柄 %d·出边 %s)" % [shop_id, str(shop_outs)], + String(runner.current_node().get("type", "")) == "shop" and shop_outs.size() == 1 \ + and String(s_byid.get(int(shop_outs[0]), {}).get("type", "")) == "boss" \ + and (s_byid.get(int(shop_outs[0]), {}).get("next", []) as Array).is_empty()) + Kit.check("L2 到店路径穿商店恰 1 家(已到句柄 %s 型别计数——必经恰一店的行进面)" % str(runner.visited_ids()), + _path_shop_count(s_nodes, runner.visited_ids()) == 1) + Kit.check("L2 商店节点(句柄 %d)入边恰一条(店位归终点的直接前驱——行进面复核)" % shop_id, shop_preds == 1) + if shop_outs.is_empty(): + Kit.check("到店商店节点无出边(夹具停摆——去终点方向无法从图解析)", false) + return Kit.check("到店开店态(覆盖层数据源就位——离开前不出选路口,零自动推进)", not runner.awaiting_choice() and runner.next_candidate_ids().is_empty()) # 现价 = 基价 + price_level_bonus(L2)=2 + 步长 × 已购档数 var prods: Array = runner.shop_products() @@ -641,11 +1050,12 @@ func _smoke_shop_room() -> void: var tier_dry: int = runner.purchased_tier("hp_up") var hp_dry: int = int(_shop_products_by_id(runner.shop_products())["hp_up"]["price"]) Kit.check("余额不足购买被拒(gp=%d < 现价 %d——零扣费零档位变动)" % [gp_dry, hp_dry], not runner.buy_product("hp_up") and runner.gp == gp_dry and runner.purchased_tier("hp_up") == tier_dry) - # 离开 → 回选路口(候选 [9]) - Kit.check("离开商店 → 回选路口(候选 [9]——零自动推进收口)", runner.continue_after_node() and runner.awaiting_choice() and runner.next_candidate_ids() == [9]) + # 离开 → 回选路口(候选 = 该店出边——恰一条,指向终点 Boss) + Kit.check("离开商店 → 回选路口(候选 = 店出边 %s——零自动推进收口)" % str(shop_outs), + runner.continue_after_node() and runner.awaiting_choice() and _sorted_ids(runner.next_candidate_ids()) == _sorted_ids(shop_outs)) # 下一战参数 = 基础表值 + 加算区(百分比已归零——加算先乘后乘序的前半在案) Kit.check("主角攻击生效值 = 2+1(基础表值 + 加算区)", runner.effective_atk() == int(_db.hero.atk) + 1) - Kit.check("选 Boss(节点 9)开战", runner.pick_next_node(9) and runner.current_battle() != null) + Kit.check("选终点 Boss(句柄 %d)开战" % int(shop_outs[0]), runner.pick_next_node(int(shop_outs[0])) and runner.current_battle() != null) var snap: Dictionary = runner.current_battle().presentation_snapshot() # max_hp 期望按实际购买笔数推导(可购笔数 # 随经济漂移;断言语义=「加算区笔数×3 全额入战斗开始参数」,不锚死恰好 1 笔) @@ -671,47 +1081,138 @@ func _shop_products_by_id(prods: Array) -> Dictionary: return out -# ---- ④ 商店房图不变式(恰 1 商店 · 必经位 · 跨表行集迁移)---- - - -# ---- ④ 商店房图不变式(恰 1 商店 · 必经位 · 跨表行集迁移)---- +# ---- ④ 商店编排性质(生成图扫描面——三条性质 + 终点跨表解析)---- func _smoke_shop_graph() -> void: - print("--- 商店房图不变式(恰 1 商店房 · 全路径必经 · 跨表行集迁移)---") - for L in [2, 3]: - var row := {} - for r in _db.level.rows: - if int(r.level_index) == L: - row = r - var shops: Array = [] - var boss_id := -1 - var max_id := -1 - for n in _level_active_nodes(L): - var t := String(n.get("type", "")) - if t == "shop": - shops.append(int(n.get("node_depth", -1))) - if t == "boss": - boss_id = int(n.get("node_depth", -1)) - max_id = maxi(max_id, int(n.get("node_depth", -1))) - Kit.check("L%d 商店房恰 1 且 node_depth=8(next=[9])" % L, shops.size() == 1 and int(shops[0]) == 8) - Kit.check("L%d Boss = 9 = 全图最大编号(商店房批重编号 8→9)" % L, boss_id == 9 and boss_id == max_id) - var paths: Array = _all_entry_boss_paths(_level_active_nodes(L)) - var via_shop := true - for p in paths: - if not p.has(8): - via_shop = false - Kit.check("L%d 全部 %d 条入口→Boss 路径穿商店房 8(必经位——DFS 枚举)" % [L, paths.size()], paths.size() >= 2 and via_shop) - # 跨表校验:(·,boss,9) Boss 行精确存在 · (·,battle,8) 无战斗行残留(陈旧行清理确认) - var bg_keys := {} - for row in _db.board_gen.rows: - bg_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true - var en_keys := {} - for row in _db.enemy.rows: - en_keys["%d_%s_%d" % [int(row.level_index), String(row.get("row_kind", "")), int(row.get("depth_band", -1))]] = true - for L in [2, 3]: - Kit.check("(%d,boss,9) Boss 行在 board_gen + enemy 精确存在" % L, bg_keys.has("%d_9" % L) and en_keys.has("%d_boss_9" % L)) - Kit.check("(%d,battle,8) 商店房无板/战斗行集残留(陈旧行已清理)" % L, not bg_keys.has("%d_8" % L) and not en_keys.has("%d_battle_8" % L)) - Kit.check("L1 行集零改动(单链 Boss (1,4)——教学期最小修订面)", bg_keys.has("1_4") and en_keys.has("1_boss_4") and not bg_keys.has("1_8") and not en_keys.has("1_battle_8")) + print("--- 商店编排性质(每终点前一位恰一店 · 店数=终点数 · 全路径恰穿一店)---") + var pred_bad := 0 + var term_total := 0 + var count_bad := 0 + var path_bad := 0 + var path_total := 0 + var graph_total := 0 + for L in [1, 2, 3]: + if _db.level_gen_params_row(L).is_empty(): + Kit.check("L%d 参数档缺行(商店编排断言无源可扫)" % L, false) + continue + for seed_val in range(SWEEP_SEED_FROM, SWEEP_SEED_TO): + var nodes: Array = _generated_graph(L, seed_val).get("nodes", []) + if nodes.is_empty(): + Kit.check("L%d 种子 %d 生成图为空(编排断言扫描面空转)" % [L, seed_val], false) + continue + graph_total += 1 + var by_id := _nodes_by_id(nodes) + var terminals := _terminal_ids(nodes) + # 性质①:每个终点的直接前驱恰为一家商店(入边一条 + 入边源型别 = shop) + for tid in terminals: + term_total += 1 + var preds: Array = [] + for n in nodes: + if (n.get("next", []) as Array).has(int(tid)): + preds.append(int(n.get("id", -1))) + if preds.size() != 1 or String(by_id.get(int(preds[0]), {}).get("type", "")) != "shop": + pred_bad += 1 + # 性质②:商店节点总数 = 终点数 + if _ids_of_type(nodes, "shop").size() != terminals.size(): + count_bad += 1 + # 性质③:所有入口→终点路径穿过且恰穿过一家店(DFS 枚举句柄序) + var paths: Array = _all_entry_terminal_paths(nodes) + if paths.size() < terminals.size(): + path_bad += 1 # 枚举面少于终点数=有终点没有入口→终点路径 + for p in paths: + path_total += 1 + if _path_shop_count(nodes, p) != 1: + path_bad += 1 + print("商店编排扫描读数:图 %d 张·终点 %d 个·入口→终点路径 %d 条" % [graph_total, term_total, path_total]) + Kit.check("编排断言扫描面非空(三关参数档 × 种子 %d–%d=图 %d 张·终点 %d·路径 %d)〔v5〕" % [SWEEP_SEED_FROM, SWEEP_SEED_TO - 1, graph_total, term_total, path_total], + graph_total == 3 * (SWEEP_SEED_TO - SWEEP_SEED_FROM) and term_total >= 3 and path_total >= 3) + Kit.check("性质① 每个终点的直接前驱恰一家商店(入边一条且源型别=shop·违例 %d/%d 终点)〔v5〕" % [pred_bad, term_total], pred_bad == 0) + Kit.check("性质② 每图商店节点数 = 终点数(违例 %d/%d 图)〔v5〕" % [count_bad, graph_total], count_bad == 0) + Kit.check("性质③ 每条入口→终点路径恰穿一家商店(违例 %d/%d 条路径)〔v5〕" % [path_bad, path_total], path_bad == 0) + # 现行起关图(种子 %d)逐关读数——L1 单终点、L2/L3 多终点(每终点前一位恰一店的当关投影) + for L in [1, 2, 3]: + var cur_nodes: Array = _level_active_nodes(L) + var cur_terms := _terminal_ids(cur_nodes) + var cur_shops := _ids_of_type(cur_nodes, "shop") + Kit.check("L%d 现行图终点 %d 个 = 商店 %d 个·入口→终点路径 %d 条全穿恰一店" % [L, cur_terms.size(), cur_shops.size(), _all_entry_terminal_paths(cur_nodes).size()], + cur_terms.size() == cur_shops.size() and not cur_terms.is_empty() + and _all_entry_terminal_paths(cur_nodes).size() >= cur_terms.size() + and _paths_all_one_shop(cur_nodes)) + # 终点跨表解析:每个终点深度的 Boss 行集解析命中且落带 ≥ 终点深度(顶 clamp 消解即红) + for L in [1, 2, 3]: + var cur_nodes2: Array = _level_active_nodes(L) + var tdepths := _terminal_depths(cur_nodes2) + var hit_ok := not tdepths.is_empty() + for d in tdepths: + if not _enemy_band_covers(L, int(d), "boss"): + hit_ok = false + Kit.check("L%d 现行图终点深度 %s 逐点 Boss 行集解析命中且落带 ≥ 终点深度" % [L, str(tdepths)], hit_ok) + # L1 现行图跨表覆盖(⑦/⑨ 只扫 L2/L3——L1 图侧覆盖在此闭合:每 battle/boss 深度敌侧落带+板侧精确命中) + var l1_cov := _coverage_scan(1, _level_active_nodes(1)) + Kit.check("L1 现行图跨表覆盖(战斗/Boss 节点 %d 颗非空·敌侧违例 %d·板侧违例 %d)" % [int(l1_cov["nodes"]), int(l1_cov["enemy_bad"]), int(l1_cov["board_bad"])], + int(l1_cov["nodes"]) >= 1 and int(l1_cov["enemy_bad"]) == 0 and int(l1_cov["board_bad"]) == 0) + +# ---- ⑥ 大岔口分区域兑付(V3——多终点图走哪条路决定打谁,任一 Boss 胜即收束恰一次)---- +func _smoke_multi_terminal_settlement() -> void: + print("--- 大岔口分区域兑付(L2 两轴各打各的终点)---") + var autoplay = load("res://sim/level_autoplay.gd") + var nodes: Array = _generated_graph(2, 42).get("nodes", []) + var boss_ids := _ids_of_type(nodes, "boss") + Kit.check("L2 现行图为多终点(Boss 终点 %s 两颗以上——大岔口承载面)" % str(boss_ids), boss_ids.size() >= 2) + var run_min: Dictionary = autoplay.run_level(_db, 42, 2, 400, 300, false, null, "normal", "min") + var run_max: Dictionary = autoplay.run_level(_db, 42, 2, 400, 300, false, null, "normal", "max") + var min_vis: Array = run_min.get("visited_ids", []) as Array + var max_vis: Array = run_max.get("visited_ids", []) as Array + var min_last := -1 + var max_last := -1 + if not min_vis.is_empty(): + min_last = int(min_vis[min_vis.size() - 1]) + if not max_vis.is_empty(): + max_last = int(max_vis[max_vis.size() - 1]) + # 走哪条路决定打谁:两路各自收在本路终点 Boss 上且互不相同;本关收束 result=victory + Kit.check("L2 min/max 两路各打进各自终点 Boss(末句柄 %d / %d 互异且都在图 Boss 集 %s 内;两路 result=%s/%s)" % [min_last, max_last, str(boss_ids), String(run_min.get("result", "")), String(run_max.get("result", ""))], + String(run_min.get("result", "")) == "victory" and String(run_max.get("result", "")) == "victory" + and boss_ids.has(min_last) and boss_ids.has(max_last) and min_last != max_last) + # 不误兑付:另一颗 Boss 不在本路已到序里(走 A 区不误兑付 B 区 Boss) + Kit.check("min 路未到 max 终点 %d、max 路未到 min 终点 %d(区域隔离——走 A 区不误兑付 B 区 Boss)" % [max_last, min_last], + not min_vis.has(max_last) and not max_vis.has(min_last)) + # 任一 Boss 胜=本关收束恰一次(level_finished 一次·载荷 result=victory 且 node_id=该终点句柄) + var fin := {"count": 0, "payload": {}} + var runner = load("res://core/level_runner.gd").new(_db, _fresh_rng(42)) + runner.set_hero_override(400, 300) # 满配档——全程驱动须打到 Boss 才收束 + runner.level_finished.connect(func(p): fin["count"] = int(fin["count"]) + 1; fin["payload"] = p) + Kit.check("L2 全程驱动启动受理", runner.start_level(2, 42)) + var st_drive := _battle_st("normal") + var guard := 40 + while guard > 0 and int(fin["count"]) == 0: + guard -= 1 + if runner.awaiting_choice(): + var ids: Array = runner.next_candidate_ids() + if ids.is_empty(): + break + runner.pick_next_node(int(ids[ids.size() - 1])) + continue + var b = runner.current_battle() + if b == null: + # 兑付停点(休整/商店/奖励直通)——先确认继续再回到选路,与 _drive_level 同口径 + if not runner.continue_after_node(): + break + continue + var inner := 40 + while inner > 0 and runner.current_battle() == b: + inner -= 1 + autoplay._autoplay_battle(runner, b, st_drive, null) + var payload: Dictionary = fin["payload"] + Kit.check("任一 Boss 胜即本关收束恰一次(count=%d·载荷 result=%s·node_id=%s 在图 Boss 集 %s 内)" % [int(fin["count"]), String(payload.get("result", "")), str(payload.get("node_id", -1)), str(boss_ids)], + int(fin["count"]) == 1 and String(payload.get("result", "")) == "victory" + and boss_ids.has(int(payload.get("node_id", -1)))) + +# 现行图全路径穿店恰一(性质③在单图上的整体读数) +func _paths_all_one_shop(nodes: Array) -> bool: + for p in _all_entry_terminal_paths(nodes): + if _path_shop_count(nodes, p) != 1: + return false + return true # ---- ⑤ 商店共享池(三选一同构/乘法价/跨渠道共账/切边/断点扩键)---- @@ -732,7 +1233,7 @@ func _smoke_shop_pool() -> void: for s in range(42, 92): var a8 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(s)) a8.set_hero_override(900, 400) - if not a8.start_adventure(s, {"first_script": true, "shop_mode": "pool"}): + if not a8.start_adventure(s, {"shop_mode": "pool"}): continue if not _adv_drive_to_shop(a8, _adv_st(), autoplay): continue @@ -744,7 +1245,7 @@ func _smoke_shop_pool() -> void: # 同种子再跑一遍采第二份首架(确定性对拍——断点货架续玩=确定性重放承载的实证面) var a8b = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(int(scen["s"]))) a8b.set_hero_override(900, 400) - if a8b.start_adventure(int(scen["s"]), {"first_script": true, "shop_mode": "pool"}) \ + if a8b.start_adventure(int(scen["s"]), {"shop_mode": "pool"}) \ and _adv_drive_to_shop(a8b, _adv_st(), autoplay): first_shelf_b = (a8b.runner().shop_shelf() as Array).duplicate(true) Kit.check("进店抽 3(货架 %d 卡——同构交互)" % first_shelf_a.size(), first_shelf_a.size() == 3) @@ -932,51 +1433,10 @@ func _adv_st() -> Dictionary: # 返回 null = 不在摇号清单的封锁格)。哈希兜底按坐标确定性散列——全缸可复现。 -# 入口→Boss 全路径枚举(DFS 迭代栈——图合法性校验保证单入口无环,路径有限) -func _all_entry_boss_paths(nodes: Array) -> Array: - var by_id := {} - var inbound := {} - for n in nodes: - by_id[int(n.get("node_depth", -1))] = n - for n in nodes: - for tid in n.get("next", []): - inbound[int(tid)] = true - var entry := -1 - for n in nodes: - if not inbound.has(int(n.get("node_depth", -1))): - entry = int(n.get("node_depth", -1)) - var paths: Array = [] - var stack: Array = [[entry]] - while not stack.is_empty(): - var path: Array = stack.pop_back() - var cur: Dictionary = by_id[path[path.size() - 1]] - var outs: Array = cur.get("next", []) - if outs.is_empty(): - paths.append(path) - continue - for t in outs: - var np := path.duplicate() - np.append(int(t)) - stack.append(np) - return paths - - - # ---- 元进度·MetaTable 表结构(行值全 provisional 归元进度设计文档配读数; # conversion 覆盖恰三,Boss 两档 first ≥ repeat)---- -# 模板感知取节点表(row.templates 非空取模板 0=现行固定图,保持既有断言语义) -func _level_active_nodes(level_index: int) -> Array: - for row in _db.level.rows: - if int(row.level_index) == level_index: - var tpls: Array = row.get("templates", []) - if not tpls.is_empty(): - return tpls[0].get("nodes", []) - return row.get("nodes", []) - return [] - - # 首遇带数据驱动定位(契约 8):该 (L, battle, enemy_id) 的最低 depth_band,禁写死坐标数字 func _first_encounter_band(level_index: int, enemy_id: String) -> int: var best := -1 @@ -991,20 +1451,6 @@ func _first_encounter_band(level_index: int, enemy_id: String) -> int: return best -# 模板感知取节点表(row.templates 非空取模板 0=现行固定图,保持既有断言语义) - - -# 该关 Boss 节点深度(单终点图的终点编号——数据驱动,禁写死坐标) -func _boss_depth(level_index: int) -> int: - for n in _level_active_nodes(level_index): - if String(n.get("type", "")) == "boss": - return int(n.get("node_depth", -1)) - return -1 - - -# 首遇带数据驱动定位(契约 8):该 (L, battle, enemy_id) 的最低 depth_band,禁写死坐标数字 - - func _has_err_category(errs: Array, category: String) -> bool: for e in errs: if String(e.get("category", "")) == category: diff --git a/game/tests/unit/core/level_table_validate_test.gd b/game/tests/unit/core/level_table_validate_test.gd new file mode 100644 index 00000000..437cdfee --- /dev/null +++ b/game/tests/unit/core/level_table_validate_test.gd @@ -0,0 +1,102 @@ +# tests/unit/core/level_table_validate_test.gd — 消费侧加载校验四检单测(迭代 35 卡② T1/V1) +# 覆盖:validate_graph 正例(多入口多终点/含岔口单终点/单链退化显式出边)+负例八类 +#(非 Boss 终点/Boss 带出边/环=回边与自环/双独立链全员可达正证/悬空出边/句柄重复/battle 缺深度/空图) +#+「勿扩勿缩」反证:商店位与入口终点个数不在本检范围(关卡节点地图 v2.1「位置约束由冒烟断言把关」)。 +# 运行:godot --headless --path game --script res://tests/unit/core/level_table_validate_test.gd(expect 0) +# 协议(tests/README):extends SceneTree + quit(code);夹具资源直构不碰生产表。 +extends SceneTree + +const Kit := preload("res://tests/unit/test_kit.gd") +const LevelTableScript := preload("res://data/schemas/level_table.gd") + + +func _init() -> void: + _test_positive() + _test_negative() + _test_scope_not_extended() + quit(Kit.finish("level_table_validate_test")) + + +func _n(id: int, type_: String, depth: int, outs: Array) -> Dictionary: + return {"id": id, "type": type_, "depth": depth, "next": outs} + + +func _errs(nodes: Array) -> Array: + return LevelTableScript.validate_graph(nodes) + + +func _has(errs: Array, cat: String, needle: String = "") -> bool: + for e in errs: + if String(e["category"]) != cat: + continue + if needle == "" or String(e["message"]).contains(needle): + return true + return false + + +# ---- 正例(四检全过)---- + +func _test_positive() -> void: + # 多入口多终点:两入口共汇入一岔口,两终点各在互异深度 + var multi := [ + _n(0, "battle", 0, [2]), _n(1, "battle", 0, [2]), + _n(2, "battle", 1, [3, 5]), + _n(3, "rest", 2, [4]), _n(4, "shop", 3, [6]), _n(6, "boss", 4, []), + _n(5, "reward", 2, [7]), _n(7, "shop", 3, [8]), _n(8, "boss", 5, [])] + Kit.check("四检正例:多入口多终点图零错误〔v1〕", _errs(multi).is_empty()) + # 含岔口单终点:岔口两侧链与主去路同深、汇入同一后继 + var forked := [ + _n(0, "battle", 0, [1]), + _n(1, "battle", 1, [2, 3]), + _n(2, "reward", 2, [4]), _n(3, "rest", 2, [4]), + _n(4, "shop", 3, [5]), _n(5, "boss", 4, [])] + Kit.check("四检正例:含岔口单终点图零错误〔v1〕", _errs(forked).is_empty()) + # 单链退化形态合法(校验不设「必有岔口」) + var chain := [_n(0, "battle", 0, [1]), _n(1, "battle", 1, [2]), _n(2, "shop", 2, [3]), _n(3, "boss", 3, [])] + Kit.check("四检正例:单链退化图零错误(不设必有岔口)〔v1〕", _errs(chain).is_empty()) + + +# ---- 负例九类(各报对应类别与判据消息)---- + +func _test_negative() -> void: + var non_boss_end := [_n(0, "battle", 0, [1]), _n(1, "battle", 1, [])] + Kit.check("负例:终点非 Boss 命中「终点恒为 Boss」〔v1〕", + _has(_errs(non_boss_end), "structural", "终点 1 非 Boss")) + var boss_out := [_n(0, "battle", 0, [1]), _n(1, "boss", 1, [2]), _n(2, "shop", 2, [])] + Kit.check("负例:Boss 带出边被拒〔v1〕", _has(_errs(boss_out), "structural", "Boss 节点 1 带出边")) + var back_edge := [_n(0, "battle", 0, [1]), _n(1, "battle", 1, [2]), _n(2, "shop", 0, [3]), _n(3, "boss", 4, [])] + Kit.check("负例:回边(目标深度不严格大于源)命中前进方向判据〔v1〕", + _has(_errs(back_edge), "structural", "违反前进方向")) + var cycle := [_n(0, "battle", 0, [1]), _n(1, "battle", 1, [2]), _n(2, "shop", 2, [1]), _n(3, "boss", 3, [])] + Kit.check("负例:环(2→1 回边)被前进方向拦下〔v1〕", _has(_errs(cycle), "structural", "违反前进方向")) + # 判据②(无孤儿无断头)与判据④(前进方向)在无环图上互斥:DAG 的每个源点都是入口、每条极长路都止于终点, + # 故 ② 是④失效时的纵深防御面,构造不出独立负例——此处以「两独立链双入口双终点」正证 ② 不误伤。 + var two_components := [ + _n(0, "battle", 0, [1]), _n(1, "shop", 1, [2]), _n(2, "boss", 2, []), + _n(3, "battle", 0, [4]), _n(4, "rest", 1, [5]), _n(5, "boss", 2, [])] + Kit.check("判据②正证:两独立链(双入口双终点)不误伤·全员可达〔v1〕", _errs(two_components).is_empty()) + var self_loop := [_n(0, "battle", 0, [1]), _n(1, "shop", 1, [2]), _n(2, "boss", 2, []), _n(3, "battle", 1, [3])] + Kit.check("判据②兜底面:自环先被前进方向拦下(④ 与 ② 互斥的实证)〔v1〕", + _has(_errs(self_loop), "structural", "违反前进方向")) + var dangling := [_n(0, "battle", 0, [9]), _n(1, "boss", 1, [])] + Kit.check("负例:悬空出边报 dangling_next〔v1〕", _has(_errs(dangling), "dangling_next")) + var dup := [_n(0, "battle", 0, [1]), _n(1, "boss", 1, []), _n(1, "shop", 2, [])] + Kit.check("负例:句柄重复报 dup_key〔v1〕", _has(_errs(dup), "dup_key")) + var no_depth := [_n(0, "battle", 0, [1]), {"id": 1, "type": "battle", "next": [2]}, _n(2, "boss", 2, [])] + Kit.check("负例:battle 节点缺深度被良构基底拒〔v1〕", _has(_errs(no_depth), "structural", "缺深度")) + Kit.check("负例:空节点集被拒〔v1〕", _has(_errs([]), "structural", "图节点集为空")) + var bad_type := [_n(0, "mine", 0, [1]), _n(1, "boss", 1, [])] + Kit.check("负例:类型出域报 enum〔v1〕", _has(_errs(bad_type), "enum")) + + +# ---- 「勿扩勿缩」反证:本检不含商店位与个数对参数档的核对 ---- + +func _test_scope_not_extended() -> void: + # 合法形状但全程无商店/三个终点同入口图——四检不该因此报错(商店位归冒烟断言、个数归产出侧) + var no_shop := [_n(0, "battle", 0, [1, 2]), _n(1, "battle", 1, [3]), _n(2, "rest", 1, [3]), _n(3, "boss", 2, [])] + Kit.check("范围反证:无商店图不在加载校验内报错(商店位由冒烟断言把关)〔v1〕", _errs(no_shop).is_empty()) + var two_entries := [_n(0, "battle", 0, [2]), _n(1, "battle", 0, [2]), _n(2, "boss", 1, [])] + Kit.check("范围反证:双入口合法(入口唯一判据已随模板池退役,个数归产出侧)〔v1〕", _errs(two_entries).is_empty()) + # 良构字段缺失(无 next 键)该报错——不属被退役判据 + var missing_key := [{"id": 0, "type": "battle", "depth": 0}, _n(1, "boss", 1, [])] + Kit.check("范围反证:节点缺 next 字段仍被良构基底拒〔v1〕", _has(_errs(missing_key), "structural", "缺字段 next")) diff --git a/game/tests/unit/core/level_table_validate_test.gd.uid b/game/tests/unit/core/level_table_validate_test.gd.uid new file mode 100644 index 00000000..24713668 --- /dev/null +++ b/game/tests/unit/core/level_table_validate_test.gd.uid @@ -0,0 +1 @@ +uid://bhymincbe2j4m diff --git a/game/tests/unit/core/meta_progression_forge_test.gd b/game/tests/unit/core/meta_progression_forge_test.gd index 9296434e..967bd3be 100644 --- a/game/tests/unit/core/meta_progression_forge_test.gd +++ b/game/tests/unit/core/meta_progression_forge_test.gd @@ -313,15 +313,15 @@ func _meta_settle_and_workshop() -> void: Kit.check("开局传参夹具前置:Fork 侧 a 激活 rule 键非空 + Y=Lv2 行表值 2(防断言空转)", rules_e.has("posture_first_strike") and mp_e.attune_refresh_charges() == 2) var p_new: Dictionary = load("res://presentation/main.gd").build_adventure_params(sv_e, mp_e, - {"first_script": true, "treasure_unlocked": {}, "shop_mode": "pool"}) + {"treasure_unlocked": {"wide_scout_band": true}, "shop_mode": "pool"}) Kit.check("主菜单开新冒险参数面:同调装配 = 存档装配集(营地装了什么开局就带什么)", (p_new.get("attunement", []) as Array) == (sv_e.attunement_equipped() as Array)) Kit.check("主菜单开新冒险参数面:Y 刷新池 = 当前等级行表值(营地升几档开局就给几次)", int(p_new.get("attune_refresh_y", -1)) == mp_e.attune_refresh_charges()) Kit.check("主菜单开新冒险参数面:树 rule 键集 = Fork 激活侧 effects(点哪侧带哪侧)", (p_new.get("tree_rules", []) as Array) == rules_e) - Kit.check("主菜单开新冒险参数面:入口特有键(first_script/shop_mode)随 extra 原样并入", - bool(p_new.get("first_script", false)) and String(p_new.get("shop_mode", "")) == "pool") + Kit.check("主菜单开新冒险参数面:入口特有键(treasure_unlocked/shop_mode)随 extra 原样并入", + bool((p_new.get("treasure_unlocked", {}) as Dictionary).get("wide_scout_band", false)) and String(p_new.get("shop_mode", "")) == "pool") var p_resume: Dictionary = load("res://presentation/main.gd").build_adventure_params(sv_e, mp_e, {"treasure_unlocked": {}}) Kit.check("主菜单继续上局参数面:三键与开新冒险同源同值(续玩不吃白板开局)", @@ -333,7 +333,7 @@ func _meta_settle_and_workshop() -> void: var adr_res = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(19)) var bp_min: Dictionary = {"level_index": 1, "node_id": -1, "hero_hp": 40, "gp": 5, "bonus": {"atk": 0, "max_hp": 0, "armor_class": 0}, "shop_purchased": {}, - "template_pick": -1, "master_seed": 777, "rng_counter": 3, "keystone": 0, + "graph_gen_v2": 1, "master_seed": 777, "rng_counter": 3, "keystone": 0, "visited_ids": []} Kit.check("继续上局受理面:resume_adventure 受理三键参数面(尾裁微批b 受理闭环——受理不再吃白板)", adr_res.resume_adventure(bp_min, p_resume)) @@ -662,6 +662,30 @@ func _smoke_tree_rules() -> void: ctx_fz2.level_bucket = lvl_shared # 挂同一 level 桶引用(新形态跨战持久:无 export/seed,同字典即持久) Kit.check("锋:限次跨战持久(level 桶共享引用——替代旧副本搬运链)〔无状态化 V4〕", (fz2.dispatch(ctx_fz2, "mine_hit", {}, Callable()) as Array).is_empty()) + # 锋·惊巢反制效果行换代断言(迭代36 u13/wa12——_grant_routed 减半分支效果行=战斗日志 + # step_row 真实生产面;display_name 玩家可见,禁字「挖雷」由此行清零钉住) + var orch_nc = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777003)) + orch_nc.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40, + "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {}, + "feats": [], "tree_rules": [], "battle_index": 1, + "bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {}, + "voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}) # 真实战斗上下文(状态容器开域——dispatch 链受理) + var nc_rows: Array = [] # step_row 采集盒(lambda 捕引用容器——捕获变量赋值不出闭包) + orch_nc.step_row.connect(func(r): nc_rows.append(r)) + # heal 负向行表值 power=6(极性字典行值——负向 power 走表值不走快照)→ halve 50% ceil=3 + orch_nc._grant_routed({"cell_type": "heal", "power": 10}, "negative", + "mine_hit", Vector2i.ZERO, 0, "", "", 50) + # 受理判据=惊巢反制效果行落 step_row(halve 分支先于 dispatch 发行——flat 行 _dispatch_row + # 恒返 false,返回值不承载受理语义;行发行即分支受理) + var nc_hit := "" + for r in nc_rows: + if String((r as Dictionary).get("display_name", "")).contains("惊巢反制"): + nc_hit = String((r as Dictionary).get("display_name", "")) + break + Kit.check("锋:惊巢反制 mine_hit 减半分支受理(wa12 前提——halve_pct=50 效果行落 step_row)〔wa12〕", + nc_hit != "") + Kit.check("惊巢反制效果行==「惊巢反制——危险格挖掘惩罚减半(6→3)」零「挖雷」(u13 换代)〔wa12〕", + nc_hit == "惊巢反制——危险格挖掘惩罚减半(6→3)" and not nc_hit.contains("挖雷")) # 守侧五场景(契约 12):编排 rig——begin_battle 携 sense_guard,授予段 _grant_seg._judge_seal 直注 var orch = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777001)) Kit.check("守侧 rig:战斗编排启动(tree_rules=sense_guard 携带)", diff --git a/game/tests/unit/core/pool_case_d_test.gd b/game/tests/unit/core/pool_case_d_test.gd index c22b3805..5f3b4639 100644 --- a/game/tests/unit/core/pool_case_d_test.gd +++ b/game/tests/unit/core/pool_case_d_test.gd @@ -34,7 +34,7 @@ func _init() -> void: # T10——空配置验收闸(三选一池空兜底/商店空货架) var e = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new()) - e.start_adventure(501, {"first_script": true}) + e.start_adventure(501) e._curio_pending = {"candidates": []} Kit.check_quiet(e.skip_empty_curio() and e._curio_pending.is_empty(), "空配置闸:三选一池空=停点整体跳过(skip_empty_curio 消化)〔专长批 T10〕") @@ -65,7 +65,7 @@ func _tiers_of(cands: Array) -> Array: func check_v7(a, db) -> void: - if not a.start_adventure(201, {"first_script": true}): + if not a.start_adventure(201): Kit.check_quiet(false, "V7 前置:冒险启动") return # 构成①:全池含优秀档以上——曝光槽应出 1 格高档(rank≥excellent) @@ -110,7 +110,7 @@ func check_v7(a, db) -> void: func check_v6(b, db) -> void: - if not b.start_adventure(202, {"first_script": true}): + if not b.start_adventure(202): Kit.check_quiet(false, "V6 前置:冒险启动") return # 拿了消耗:直置停点候选后 pick_curio——牌库摘牌单点 @@ -139,7 +139,7 @@ func check_v6(b, db) -> void: func check_v12(m, db) -> void: - if not m.start_adventure(401, {"first_script": true}): + if not m.start_adventure(401): Kit.check_quiet(false, "V12 前置:冒险启动") return var tier_n := {"common": 0, "excellent": 0, "rare": 0, "legendary": 0} @@ -157,7 +157,7 @@ func check_v12(m, db) -> void: func check_v8(c, db) -> void: - if not c.start_adventure(203, {"first_script": true}): + if not c.start_adventure(203): Kit.check_quiet(false, "V8 前置:冒险启动") return # 概率位与 cap 两属性独立:改 cap 不动牌库(概率位)、改牌库不动 cap 字段 diff --git a/game/tests/unit/core/save_manager_domain_test.gd b/game/tests/unit/core/save_manager_domain_test.gd index 20cc3315..d33c1ca2 100644 --- a/game/tests/unit/core/save_manager_domain_test.gd +++ b/game/tests/unit/core/save_manager_domain_test.gd @@ -184,7 +184,7 @@ func _smoke_save() -> void: "level_index": 2, "node_id": 5, "hero_hp": 13, "bonus": {"atk": 2, "max_hp": 5, "armor_class": 1}, "gp": 17, "shop_purchased": {"hp_small": 2, "atk_small": 1}, - "template_pick": 3, + "graph_gen_v2": 1, "master_seed": 424242, "rng_counter": 38, "keystone": 6, "visited_ids": [0, 2, 5], "aux_items": [{"item_id": "aux_scout", "source_class": "reward_node", @@ -205,7 +205,7 @@ func _smoke_save() -> void: var bp_shop: Dictionary = bp.get("shop_purchased", {}) Kit.check("断点七字段往返:商店已购档", int(bp_shop.get("hp_small", -1)) == 2 and int(bp_shop.get("atk_small", -1)) == 1) - Kit.check("断点七字段往返:已抽模板", int(bp.get("template_pick", -1)) == 3) + Kit.check("断点七字段往返:生成图代差标记键", int(bp.get("graph_gen_v2", 0)) == 1) Kit.check("断点附随件往返:主种子/计数器/钥石累计", int(bp.get("master_seed", -1)) == 424242 and int(bp.get("rng_counter", -1)) == 38 and int(bp.get("keystone", -1)) == 6) @@ -237,7 +237,7 @@ func _smoke_save() -> void: fv2f.store_string(JSON.stringify({"version": 2, "meta": {"keystone_balance": 7}, "breakpoint": {"level_index": 1, "node_id": -1, "hero_hp": 40, "gp": 5, "bonus": {"atk": 0, "max_hp": 0, "armor_class": 0}, "shop_purchased": {}, - "template_pick": -1, "master_seed": 777, "rng_counter": 3, "keystone": 0, + "graph_gen_v2": 1, "master_seed": 777, "rng_counter": 3, "keystone": 0, "visited_ids": []}})) fv2f.close() Kit.check("v2 旧档迁移 v3:断点域与 meta 域原样保留(缺新附随件=合法空态)", diff --git a/game/tests/unit/data_config/table_structure_guard_test.gd b/game/tests/unit/data_config/table_structure_guard_test.gd index 7ec9c00d..285d5bad 100644 --- a/game/tests/unit/data_config/table_structure_guard_test.gd +++ b/game/tests/unit/data_config/table_structure_guard_test.gd @@ -72,14 +72,6 @@ static func _errs_has_cross_field(errs: Array) -> bool: return false -# 该关 Boss 节点深度(单终点图的终点编号——数据驱动,禁写死坐标) -func _boss_depth(level_index: int) -> int: - for n in _level_active_nodes(level_index): - if String(n.get("type", "")) == "boss": - return int(n.get("node_depth", -1)) - return -1 - - # 首遇带数据驱动定位(契约 8):该 (L, battle, enemy_id) 的最低 depth_band,禁写死坐标数字 func _first_encounter_band(level_index: int, enemy_id: String) -> int: var best := -1 @@ -94,17 +86,6 @@ func _first_encounter_band(level_index: int, enemy_id: String) -> int: return best -# 模板感知取节点表(row.templates 非空取模板 0=现行固定图,保持既有断言语义) -func _level_active_nodes(level_index: int) -> Array: - for row in _db.level.rows: - if int(row.level_index) == level_index: - var tpls: Array = row.get("templates", []) - if not tpls.is_empty(): - return tpls[0].get("nodes", []) - return row.get("nodes", []) - return [] - - # ① 冒险表(单行全局 Rule 4 豁免——建表与接线闸;TP0 令牌池:配额结构/件值对/池总量域检) func _smoke_adventure_table() -> void: print("--- 冒险表(AdventureTable——令牌池行值单点:回血/配额/件值对/同调位)---") @@ -222,26 +203,31 @@ func _smoke_adventure_table() -> void: v18_ok and locked_n == 12) -# ---- 敌人表深度带键结构(迭代 34:20 行键迁移 · 键序=迁移表序 · hobgoblin 留任 · 三 Boss 行 kind/band)---- +# ---- 敌人表深度带键结构(迭代 34 建·迭代 35 覆盖批换代:28 行键序=覆盖表序·hobgoblin 留任·Boss 行集带键清单)---- func _smoke_enemy_bands() -> void: - print("--- 敌人表深度带键结构(20 行键序 · hobgoblin 留任 · 三 Boss 行)---") - # ① 20 行计数 + 键序 = 迁移表序(文件行序不变——行序即契约) + print("--- 敌人表深度带键结构(28 行键序 · hobgoblin 留任 · Boss 带键覆盖表)---") + # ① 28 行计数 + 键序 = 覆盖表序(文件行序不变——行序即契约) + # 迭代 35 生成图深度扩面覆盖批随动:战斗带顶扩到各关最大生成深度(L1→6/L2→9/L3→13)、 + # Boss 行按终点深度分带(L1 6/L2 8·9/L3 11·12·13),模板时代带键 4/9/9 全部在册不动 var expect_keys: Array[String] = [ "1|battle|0|goblin", "1|battle|1|goblin", "1|battle|2|bugbear", "1|battle|3|hobgoblin", - "1|boss|4|ogre_chief", + "1|battle|6|bugbear", + "1|boss|4|ogre_chief", "1|boss|6|ogre_chief", "2|battle|0|bugbear", "2|battle|1|bugbear", "2|battle|4|giant_spider", "2|battle|5|bugbear", "2|battle|5|giant_spider", "2|battle|6|giant_spider", "2|battle|7|giant_spider", - "2|boss|9|drider", + "2|battle|9|giant_spider", + "2|boss|8|drider", "2|boss|9|drider", "3|battle|0|duergar", "3|battle|1|hook_horror", "3|battle|4|hook_horror", "3|battle|5|duergar", - "3|battle|6|hook_horror", "3|battle|7|duergar", "3|boss|9|duergar_king", + "3|battle|6|hook_horror", "3|battle|7|duergar", "3|battle|13|duergar", + "3|boss|9|duergar_king", "3|boss|11|duergar_king", "3|boss|12|duergar_king", "3|boss|13|duergar_king", ] var got_keys: Array[String] = [] for row in _db.enemy.rows: got_keys.append("%d|%s|%d|%s" % [int(row.get("level_index", -1)), String(row.get("row_kind", "")), int(row.get("depth_band", -1)), String(row.get("enemy_id", ""))]) - Kit.check("敌人表 20 行且键序 = 迁移表序((L,row_kind,depth_band,enemy_id) 逐行比对——文件行序不变)〔fv11〕", - _db.enemy.rows.size() == 20 and got_keys == expect_keys) + Kit.check("敌人表 28 行且键序 = 覆盖表序((L,row_kind,depth_band,enemy_id) 逐行比对——文件行序不变)〔fv11〕", + _db.enemy.rows.size() == 28 and got_keys == expect_keys) # ② hobgoblin 缺口行留任随迁(契约 9:不删行不补立绘——缺口登记随行) var hob := {} for row in _db.enemy.rows: @@ -252,23 +238,27 @@ func _smoke_enemy_bands() -> void: and int(hob.get("depth_band", -1)) == 3) Kit.check("hobgoblin 缺口登记随行(表文件注释在位——缺口不静默)〔fv25〕", FileAccess.get_file_as_string("res://data/tables/enemy.tres").contains("hobgoblin 行为缺口行")) - # ③ 三 Boss 行 kind/band 就位(band = 该关图 Boss 节点深度——单终点图数据驱动) + # ③ Boss 行集带键 = 覆盖表(旧判据「各恰一行=图 Boss 节点深度」属单终点模板时代; + # 生成图多终点要求每终点深度解析到互异带键 ⇒ 每关 Boss 行必然多于一行—— + # 迭代 35 覆盖批随动改写为带键清单,「带键互异」的运行时把关在 level_shop_economy_test〔v11〕) + var boss_bands := {} + for row in _db.enemy.rows: + if String(row.get("row_kind", "")) == "boss": + var lk: int = int(row.get("level_index", -1)) + if not boss_bands.has(lk): + boss_bands[lk] = [] + boss_bands[lk].append(int(row.get("depth_band", -1))) + var expect_bands := {1: [4, 6], 2: [8, 9], 3: [9, 11, 12, 13]} var boss_ok := true var boss_detail := "" for L in [1, 2, 3]: - var bdepth := _boss_depth(L) - var hits := 0 - for row in _db.enemy.rows: - if int(row.get("level_index", -1)) == L and String(row.get("row_kind", "")) == "boss": - hits += 1 - if int(row.get("depth_band", -1)) != bdepth: - boss_ok = false - boss_detail += " L%d:%d≠%d" % [L, int(row.get("depth_band", -1)), bdepth] - if hits != 1: + var got: Array = boss_bands.get(L, []) + got.sort() + if got != expect_bands[L]: boss_ok = false - boss_detail += " L%d:boss 行×%d" % [L, hits] - Kit.check("三关 Boss 行各恰一行且 row_kind=boss、band=图 Boss 节点深度(%d/%d/%d%s)〔fv13〕" % [ - _boss_depth(1), _boss_depth(2), _boss_depth(3), boss_detail], boss_ok) + boss_detail += " L%d:%s≠%s" % [L, str(got), str(expect_bands[L])] + Kit.check("三关 Boss 行集带键 = 覆盖表(模板终点 4/9/9 在册+生成图终点 6/8/9/11/12/13 扩带)%s〔fv13〕" % boss_detail, + boss_ok) # ② 免伤墙归零护栏泛化保留(BUG-0003 事故类永久免疫):hero_defense 池任何 percent 行 diff --git a/game/tests/unit/presentation/level_map_info_fog_test.gd b/game/tests/unit/presentation/level_map_info_fog_test.gd index 732900f9..56caa3b0 100644 --- a/game/tests/unit/presentation/level_map_info_fog_test.gd +++ b/game/tests/unit/presentation/level_map_info_fog_test.gd @@ -29,7 +29,7 @@ func _run() -> void: quit(Kit.finish("level_map_info_fog_test")) -# 白盒 runner:夹具行集注入 db.level + level_row,入口停点态(node_index=-1,候选=入边首节点) +# 白盒 runner:夹具行集注入 db.level + level_row,入口停点态(node_index=-1,候选=首层首节点句柄 1) func _runner(db, nodes: Array, candidates: Array, n: int = 2): var t := LevelTableScript.new() t.info_visible_layers = n @@ -37,7 +37,6 @@ func _runner(db, nodes: Array, candidates: Array, n: int = 2): db.level = t var r = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new()) r.level_row.assign({"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30, "nodes": nodes}) - r._graph_mode = true r.node_index = -1 r._candidates.assign(candidates) return r @@ -46,7 +45,7 @@ func _runner(db, nodes: Array, candidates: Array, n: int = 2): # ---- ① 范围外只见结构:雾下节点类型图标与文字均不绘制〔fv2〕---- func _test_fog_hides_type() -> void: - var node := {"node_depth": 3, "type": "battle"} + var node := {"id": 3, "type": "battle", "depth": 3, "next": []} var shown: Dictionary = ScreenScript.node_display(node, 2, [2], false, false) Kit.check("可见集内节点:图标/类型文字均绘制〔fv2〕", bool(shown["draw_icon"]) and bool(shown["draw_label"])) @@ -60,24 +59,24 @@ func _test_fog_hides_type() -> void: func _test_boss_exempt_and_multi_boss() -> void: var type_vis: Array = [1, 7, 9] # 双 Boss 图:7/9 均在可见集 - var boss_a: Dictionary = ScreenScript.node_display({"node_depth": 7, "type": "boss"}, 7, type_vis, false, false) - var boss_b: Dictionary = ScreenScript.node_display({"node_depth": 9, "type": "boss"}, 9, type_vis, false, false) + var boss_a: Dictionary = ScreenScript.node_display({"id": 7, "type": "boss", "depth": 7, "next": []}, 7, type_vis, false, false) + var boss_b: Dictionary = ScreenScript.node_display({"id": 9, "type": "boss", "depth": 9, "next": []}, 9, type_vis, false, false) Kit.check("多终点:两个 Boss 节点均带 Boss 描边〔fv3〕", bool(boss_a["boss_border"]) and bool(boss_b["boss_border"])) Kit.check("Boss 不入雾:可见集与图标/文字三通道均在〔fv3〕", bool(boss_a["draw_icon"]) and bool(boss_a["draw_label"]) and bool(boss_b["draw_icon"])) - var far_boss: Dictionary = ScreenScript.node_display({"node_depth": 9, "type": "boss"}, 9, [1], false, false) + var far_boss: Dictionary = ScreenScript.node_display({"id": 9, "type": "boss", "depth": 9, "next": []}, 9, [1], false, false) Kit.check("Boss 豁免独立于可见集:不在 type_vis 的 Boss 仍出示类型〔fv3〕", bool(far_boss["draw_icon"]) and bool(far_boss["draw_label"])) - var fogged_battle: Dictionary = ScreenScript.node_display({"node_depth": 5, "type": "battle"}, 5, type_vis, false, false) + var fogged_battle: Dictionary = ScreenScript.node_display({"id": 5, "type": "battle", "depth": 5, "next": []}, 5, type_vis, false, false) Kit.check("Boss 豁免不扩散:雾下非 Boss 仍隐类型〔fv3〕", not bool(fogged_battle["draw_icon"])) # ---- ③ 节点内详情图上从不显示〔fv5〕---- func _test_details_never_shown() -> void: - var plain := {"node_depth": 3, "type": "battle"} - var loaded := {"node_depth": 3, "type": "battle", "enemy_id": "goblin_secret", "reward_id": "reward_secret"} + var plain := {"id": 3, "type": "battle", "depth": 3, "next": [4]} + var loaded := {"id": 3, "type": "battle", "depth": 3, "next": [4], "enemy_id": "goblin_secret", "reward_id": "reward_secret"} var a: Dictionary = ScreenScript.node_display(plain, 3, [3], false, true) var b: Dictionary = ScreenScript.node_display(loaded, 3, [3], false, true) var same := true @@ -92,7 +91,7 @@ func _test_details_never_shown() -> void: # ---- ④ label_text 三态文案〔fv2〕---- func _test_label_three_states() -> void: - var node := {"node_depth": 3, "type": "battle"} + var node := {"id": 3, "type": "battle", "depth": 3, "next": [4]} Kit.check("当前位文案=「当前位置」〔fv2〕", String(ScreenScript.node_display(node, 3, [3], true, false)["label_text"]) == "当前位置") Kit.check("候选文案=「战斗 · 可选」〔fv2〕", @@ -103,9 +102,10 @@ func _test_label_three_states() -> void: # ---- 大图(11 层×6 槽含双 Boss)画布拟合与布局包络〔fv8〕---- -# 大图夹具:编号按 12 层×6 槽构造,层 0 入口、末层双 Boss;含层 1 首节点→第 3 层节点的跨层跳边。 -# 跳边目标按 BFS 最短路径提前一层,其后继链随之提前——布局落 11 层(最大槽 7: -# 跳边目标并入第 2 层槽组)。断言按布局实测取最大层/最大槽,画布公式与之同源。 +# 大图夹具:按 12 层×6 槽构造(id = 图内句柄 = 构造序号;depth 随构造序号严格前进——生成图连线铁律), +# 层 0 入口、末层双 Boss(id 61/62,句柄互异、next 恒空);含层 1 首节点→第 3 层节点的跨层长跳边。 +# 跳边目标按 BFS 最短路径提前一层,其后继链随之逐层提前(目标并入第 2 层槽组)——布局落 11 层。 +# 断言按布局实测取最大层/最大槽,画布公式与之同源。 func _big_graph_nodes() -> Array: var layers: Array = [] var total := 0 @@ -118,30 +118,25 @@ func _big_graph_nodes() -> Array: total += 1 layers.append(ids) var nodes: Array = [] - var jump_from := -1 - var jump_to := -1 for l in tiers: for i in layers[l].size(): var nid: int = layers[l][i] - var n := {"node_depth": nid} - if l == 0: - n["type"] = "battle" - n["next"] = layers[1].duplicate() # 入口扇出到全层(层宽 6) - elif l == tiers - 1: - n["type"] = "boss" - n["next"] = [] + var t := "battle" + var next_ids: Array = [] + if l == tiers - 1: + t = "boss" + elif l == 0: + next_ids = layers[1].duplicate() # 入口扇出到全层(层宽 6) else: if i % 5 == 0: - n["type"] = "reward" if l % 2 == 0 else "rest" - else: - n["type"] = "battle" + t = "reward" if l % 2 == 0 else "rest" var w2: int = layers[l + 1].size() - n["next"] = [layers[l + 1][i % w2], layers[l + 1][(i + 1) % w2]] - nodes.append(n) - jump_from = layers[1][0] - jump_to = layers[3][2] + next_ids = [layers[l + 1][i % w2], layers[l + 1][(i + 1) % w2]] + nodes.append({"id": nid, "type": t, "depth": nid, "next": next_ids}) + var jump_from: int = layers[1][0] + var jump_to: int = layers[3][2] for n in nodes: - if int(n["node_depth"]) == jump_from: + if int(n["id"]) == jump_from: (n["next"] as Array).append(jump_to) break return nodes @@ -188,9 +183,9 @@ func _test_big_graph_fit(db) -> void: var boss_ids: Array = [] for node in nodes: if String(node["type"]) == "boss": - boss_ids.append(int(node["node_depth"])) + boss_ids.append(int(node["id"])) Kit.check("大图呈现:两个终点 Boss 均有描边标记〔fv8〕", disp.has(boss_ids[0]) and disp.has(boss_ids[1]) and bool(disp[boss_ids[0]]["boss_border"]) and bool(disp[boss_ids[1]]["boss_border"])) - Kit.check("大图呈现:入口停点远处战斗节点(编号 32,层 6)类型入雾〔fv8〕", not bool(disp[32]["draw_icon"])) + Kit.check("大图呈现:入口停点远处战斗节点(句柄 32,层 6)类型入雾〔fv8〕", not bool(disp[32]["draw_icon"])) screen.queue_free() diff --git a/game/tests/unit/presentation/player_copy_sweep_test.gd b/game/tests/unit/presentation/player_copy_sweep_test.gd new file mode 100644 index 00000000..882d3d61 --- /dev/null +++ b/game/tests/unit/presentation/player_copy_sweep_test.gd @@ -0,0 +1,174 @@ +# tests/unit/presentation/player_copy_sweep_test.gd — 玩家面文案清扫直建断言宿主(迭代36 卡A) +# 断言面 = 玩家面字符串字面量换代钉值(u1–u13 钉值表——分类词/渠道旧话/「雷」族禁字清零): +# ① hero 面板两段头(专长/属性值提升——同屏分类撞车消除) +# ② 奖励节点三选一头部(冒险牌库/渠道分流三机制事实,零「同权重/同上限/提升池」旧话) +# ③ 专长卡兜底(feat_card_text 空行兜底「专长\n???」) +# ④ 图例封印行(标空/受挖掘惩罚,零「插错/挖雷」) +# ⑤ 状态槽来源名(mine_hit→危险格——tooltip 消费半) +# ⑥ 结算来源名(settlement 表值经 ConfigDB 真装载+battle_log 行消费半) +# ⑦ 已开危险格字形(glyph「危」+plate+RED_MINE——BoardView rebuild 真实刷新路径) +# ⑧ 源 token 扫描一:board_view.gd 零 "雷" 字面量(哨兵族统一钉住) +# ⑩ 源 token 扫描二:battle_orchestrator.gd 玩家面字符串字面量零「挖雷」(注释剥离=行首 # 判) +# 扫域口径:玩家面字符串字面量(契约 2 保留面——注释/内部常量名与注记/测试消息串不入域)。 +# 协议照单元侧惯例:extends SceneTree+Kit.check 主通道;--script 模式无 autoload, +# ConfigDB 显式实例化构造注入(统一流 master 种子 42 沿源)。 +# 运行:godot --headless --path game --script res://tests/unit/presentation/player_copy_sweep_test.gd +extends SceneTree + +const Kit := preload("res://tests/unit/test_kit.gd") +const ConfigDBScript := preload("res://core/config_db.gd") +const AutoPlayer := preload("res://sim/auto_player.gd") # 自动玩家策略单一实现(夹具开域驱动) + +var _db + + +func _init() -> void: + _db = ConfigDBScript.new() + var load_ok: bool = _db.load_tables("res://data/tables", true) + if not load_ok: + push_error("[player_copy_sweep_test] 表加载返回失败——诊断如下(红由具体断言落)") + for d in _db.diagnostics(): + if d.severity == "error" or d.severity == "fatal": + push_error(" [%s] %s: %s" % [d.severity, d.category, d.message]) + _sweep_hero_panel() + _sweep_narrator() + _sweep_legend() + _sweep_source_names() + _sweep_board_glyph() + _sweep_token_scans() + quit(Kit.finish("player_copy_sweep_test")) + + +# ① C 区两段头(u1/u2——直建 hero 面板,adventure_feats_test ⑦c 族同构) +func _sweep_hero_panel() -> void: + var panel = load("res://presentation/views/hero_info_panel_view.gd").new() + var feat_head := _first_label(panel._feat_section) + var curio_head := _first_label(panel._curio_section) + Kit.check("C 区专长段头==「专长」(u1 换代——段头文案钉值)〔wa1〕", + feat_head != null and feat_head.text == "专长") + Kit.check("C 区属性值提升段头==「属性值提升」(u2 换代——同屏两段同标撞车消除)〔wa1〕", + curio_head != null and curio_head.text == "属性值提升") + + +# ②③ 奖励节点头部+专长卡兜底(u3/u11——静态直调) +func _sweep_narrator() -> void: + var narrator = load("res://presentation/battle_narrator.gd") + var head := String(narrator.node_curio_header()) + Kit.check("奖励点头部含「冒险牌库」(u3 机制事实①同库)〔wa4〕", head.contains("冒险牌库")) + Kit.check("奖励点头部含「属性值提升与补给」(u3 机制事实②渠道分流)〔wa4〕", head.contains("属性值提升与补给")) + Kit.check("奖励点头部含「同库分流」(u3 机制事实②措辞)〔wa4〕", head.contains("同库分流")) + Kit.check("奖励点头部零「同权重」旧话(u3 清零)〔wa4〕", not head.contains("同权重")) + Kit.check("奖励点头部零「同上限」旧话(u3 清零)〔wa4〕", not head.contains("同上限")) + Kit.check("奖励点头部零「提升池」旧话(u3 清零)〔wa4〕", not head.contains("提升池")) + Kit.check("专长卡兜底==「专长\n???」(u11 换代——数据缺行真实兜底路径)〔wa3〕", + String(narrator.feat_card_text({})) == "专长\n???") + + +# ④ 图例封印行(u4——直建 LegendOverlay 读 ENTRIES 常量) +func _sweep_legend() -> void: + load("res://presentation/views/legend_overlay.gd").new() # 直建(_init(rap_cost) 现签名) + var seal_row: Array = [] + for e in load("res://presentation/views/legend_overlay.gd").ENTRIES: + if String(e[1]) == "封印": + seal_row = e + break + Kit.check("图例封印行存在(wa5 夹具前提)〔wa5〕", not seal_row.is_empty()) + Kit.check("图例封印行含「标空」(u4——与「标中」成对)〔wa5〕", String(seal_row[2] if seal_row.size() > 2 else "").contains("标空")) + Kit.check("图例封印行含「受挖掘惩罚」(u4 雷字清零换代)〔wa5〕", String(seal_row[2] if seal_row.size() > 2 else "").contains("受挖掘惩罚")) + Kit.check("图例封印行零「插错」(u4 旗族遗留词干清零)〔wa5〕", not String(seal_row[2] if seal_row.size() > 2 else "").contains("插错")) + Kit.check("图例封印行零「挖雷」(u4 雷字清零)〔wa5〕", not String(seal_row[2] if seal_row.size() > 2 else "").contains("挖雷")) + + +# ⑤⑥ 来源名双源(u5/u6——状态槽消费半+表值 ConfigDB 真装载+battle_log 行消费半) +func _sweep_source_names() -> void: + var row_view = load("res://presentation/views/status_slot_row.gd").new() + row_view.set_effects([{"effect_id": "alert", "source_class": "mine_hit", + "display_name": "危险格受击", "remaining_settlements": 2, "round_index": 1}]) + var slot: PanelContainer = null + for child in row_view.get_children(): + if child is PanelContainer and child.has_meta("eff"): + slot = child + break + Kit.check("状态槽 mine_hit 效果行在座(wa6 夹具前提)〔wa6〕", slot != null) + if slot != null: + row_view._on_slot_hover(slot) + Kit.check("状态槽来源注记==「危险格」(u5 换代——meta tooltip 消费半)〔wa6〕", + row_view._tip_label.text.contains("来源:危险格")) + row_view._hide_tip() + Kit.check("结算表 source_label_map[mine_hit]==「危险格」(u6 换代——ConfigDB 真装载)〔wa7〕", + String(_db.settlement.source_label_map.get("mine_hit", "")) == "危险格") + var log_view = load("res://presentation/views/battle_log_view.gd").new(_db) + var row_txt := String(log_view._format_row({"type": "effect", "source_class": "mine_hit", + "display_name": "挖掘反噬", "value": -5, "pool_value": -5, "round_index": 1})) + Kit.check("战斗日志 mine_hit 来源行含「(危险格)」(u6 消费半——_format_row 生产路径)〔wa7〕", + row_txt.contains("(危险格)")) + + +# ⑦⑧ 已开危险格字形族(u7/u8/u9——BoardView rebuild 真实刷新路径+源 token 扫描) +func _sweep_board_glyph() -> void: + var board_generator = load("res://core/board_generator.gd").new() + var b = board_generator.generate(1, 0, _mk_stream("board_gen", 4242), _db) + var ae = load("res://core/action_economy.gd").new(_db) + var ms = load("res://core/minesweeper_rules.gd").new(_db, ae) + ms.install_board(b) + ae.notify_round_start() + # 邻接雷定位(挖掘面规则=须邻接英雄——novice 驱动开域至危险格入挖掘射程;上限 60 步防冻态) + var mine_cell := Vector2i(-1, -1) + var glow_steps := 0 + while glow_steps < 60: + for c in b.neighbors8(ms.hero_pos()): + if b.is_mine(c) and not ms.is_revealed(c): + mine_cell = c + break + if mine_cell.x >= 0: + break + var act: Dictionary = AutoPlayer.novice_action(ms) + if String(act.get("kind", "stop")) == "stop": + break + var op := String(act.get("kind", "")) + if op == "seal": + op = "seal_place" + ms.settle_batch([{"action": op, "cell": act.get("cell")}]) + glow_steps += 1 + Kit.check("棋盘夹具含挖掘射程内未开危险格(wa8 前提——实测 %d 步)〔wa8〕" % glow_steps, mine_cell.x >= 0) + if mine_cell.x >= 0: + ms.settle_batch([{"action": "dig", "cell": mine_cell}]) # 挖开危险格(真实规则路径——受击开格) + var view = load("res://presentation/views/board_view.gd").new() + view.rebuild(b, ms) # theme_projection_test.gd:200 先例式直建(缺素材程序化回退态——fresh checkout 冒烟同纪律) + var u: Node2D = view._cells[mine_cell] + Kit.check("已开危险格 glyph==「危」(u7 换代)〔wa8〕", u.glyph == "危") + Kit.check("已开危险格 plate==true(u9 牌载哨兵包含项随动)〔wa8〕", u.plate == true) + Kit.check("已开危险格 glyph_color==RED_MINE(u7 墨色不随字形换代漂移)〔wa8〕", + u.glyph_color == UiKit.RED_MINE) + + +# ⑧⑩ 源 token 扫描(注释剥离=行首 # 判——契约 2 保留面不入域) +func _sweep_token_scans() -> void: + var board_src := FileAccess.get_file_as_string("res://presentation/views/board_view.gd") + Kit.check("board_view.gd 源零 \"雷\" 字面量(u7/u8/u9 哨兵族统一钉住)〔wa8〕", + board_src.count("\"雷\"") == 0) + var orch_src := FileAccess.get_file_as_string("res://core/battle_orchestrator.gd") + var orch_hits := 0 + for line in orch_src.split("\n"): + if line.strip_edges().begins_with("#"): + continue + if line.contains("挖雷"): + orch_hits += 1 + Kit.check("battle_orchestrator.gd 玩家面字符串字面量零「挖雷」(u13——注释保留面不入域)〔wa12〕", + orch_hits == 0) + + +func _first_label(node: Node) -> Label: + for child in node.get_children(): + if child is Label: + return child + return null + + +func _mk_stream(stream_name: String, scope: int) -> RandomNumberGenerator: + # stream_name = 调用点自述标签(不参与派生),保留参数零改调用方(theme_projection_test 同构) + var rg := RandomNumberGenerator.new() + var svc = load("res://core/rng_service.gd").new() + svc.set_master_seed(42) + rg.seed = svc.derive_at(scope) + return rg diff --git a/game/tests/unit/presentation/player_copy_sweep_test.gd.uid b/game/tests/unit/presentation/player_copy_sweep_test.gd.uid new file mode 100644 index 00000000..128d6d7f --- /dev/null +++ b/game/tests/unit/presentation/player_copy_sweep_test.gd.uid @@ -0,0 +1 @@ +uid://bavqeaorfbb1j diff --git a/game/tests/unit/presentation/theme_projection_test.gd b/game/tests/unit/presentation/theme_projection_test.gd index f78df38f..dd71c5b6 100644 --- a/game/tests/unit/presentation/theme_projection_test.gd +++ b/game/tests/unit/presentation/theme_projection_test.gd @@ -1,7 +1,8 @@ # tests/unit/presentation/theme_projection_test.gd — 呈现投影与主题单元测试宿主 -# 断言面 = 十套(批 B 断言迁移自 smoke_all.gd,G11 施工组;十套按源编排序迁入): +# 断言面 = 十一套(批 B 断言迁移自 smoke_all.gd,G11 施工组;十套按源编排序迁入+开场行后缀尾接协议扩段): # 未开格类型微光(作用集恒等/九型归组封闭/零玩法写入——board_view 微光数据面) # 战斗日志视图行格式化(行协议 9 类呈现层同步闸——真人试玩卡断 bug 回归) +# 开场行后缀尾接协议(level_open_line 第五参布尔直控「(含面板加成)」尾接——签名零变) # UI 全局主题(main_theme.tres 挂载/类型覆盖/滑轨两段对比/退役实现零残留/中文字体防御) # UI 按钮五态矩阵(四工厂五态样式盒逐实例 override) # UI 全局纹理过滤(工程 Nearest+平滑渐变件 LINEAR 白名单+光柱 alpha 结构) @@ -42,6 +43,7 @@ func _init() -> void: # 套执行序沿源编排序(_run_core_smoke 序 + _smoke_meta 薄壳尾序) _smoke_type_glow() _smoke_battle_log_view() + _smoke_level_open_line() _smoke_global_theme() _smoke_button_five_states() _smoke_texture_filter() @@ -420,6 +422,19 @@ func _smoke_battle_log_view() -> void: view.parsed_text().length() > before) +# ---- 开场行后缀尾接协议(迭代36 wc1 静态半——level_open_line 第五参布尔直控尾接,签名零变; +# 该函数此前零既有断言,尾接行为由此钉住)---- +func _smoke_level_open_line() -> void: + var NarratorScript := load("res://presentation/battle_narrator.gd") + Kit.check("开场行 boosted=true 尾接「(含面板加成)」〔wc1〕", + String(NarratorScript.level_open_line("第一层", 42, 5, 30, true)).contains("(含面板加成)")) + Kit.check("开场行 boosted=false 零后缀(白板局不带)〔wc1〕", + not String(NarratorScript.level_open_line("第一层", 42, 5, 30, false)).contains("(含面板加成)")) + Kit.check("开场行种子/两值带入不受后缀位影响(数值带入回归)〔wc1〕", + String(NarratorScript.level_open_line("第一层", 42, 5, 30, true)).contains("种子 42") + and String(NarratorScript.level_open_line("第一层", 42, 5, 30, true)).contains("攻击 5 / 生命 30")) + + # ---- UI 全局主题冒烟(需求卡片 §7 验收清单 V1-V6)---- # V1 挂载覆盖 / V2 滑轨两段 / V3 预盖件运行时证词 / V4 旧形态零残留 / # V5 回归(本冒烟全绿即回归网)/ V6 中文字体防御(挂主题后 default_font 顶掉 custom_font 也不丢字形) @@ -731,63 +746,64 @@ func _smoke_diag_button_retired() -> void: dbg_src.contains('UiKit.button("转储")')) -# ---- 状态面板只读投影一致性(R4 数据只读同源:状态面板展示值逐项 == MetaProgression -# 访问器读数;投影拼装与 main._status_data 同构(同访问器族同序)。只读性连接巡检与零动效 -# 断言归 ui_struct_probe 呈现侧,本段只管数据面)---- +# ---- 状态面板·本局信息面板投影一致性(迭代36 卡③——八键快照手拼夹具:展示值逐项 == +# 快照钉值;投影拼装口径=组合根 _status_data 八键(数据只读同源)。只读性连接巡检与零动效 +# 断言归 ui_struct_probe 呈现侧,本段只管数据面;跨关解锁概览断言族随面板换代退役—— +# 替换判据=sv5 零旧段+八键投影断言(实现报告 §8.3 申报))---- func _status_panel_projection() -> void: - print("--- 状态面板·只读投影一致性(隔离档 smoke_meta_buy 复用)---") - _meta_smoke_cleanup() - var sv = _meta_new_save(_META_SMOKE_PATHS[0]) - var mp = load("res://core/meta_progression.gd").new(_db, sv) - # 已购态夹具:余额非零 + 秘宝匣首件解锁——投影可辨非全零白板 - sv.add_keystone(37) - sv.unlock_item("morning_order") - sv.write_to_disk() - # 投影拼装(main._status_data 同构)+ 视图喂入 - var data := {"balance": mp.keystone_balance(), "workshop": mp.treasure_shop_status(), - "tree": mp.tree_camp_rows(), "fork_groups": mp.tree_fork_groups()} + print("--- 状态面板·本局信息面板投影一致性(迭代36 卡③——八键快照手拼夹具,同构口径不依赖 runner)---") + _meta_smoke_cleanup() # 套件级夹具清场(生涯页 fixture 依赖此前置——旧状态段同款保留) + # 夹具(代拍板 4——任意非零可辨值,禁读 data/tables 现值、禁与属性行断言共值致误判掩盖) + var data := {"balance": 77, "hero_hp": 13, "max_hp": 29, "atk": 7, "ac": 11, "gp": 41, + "curios": [{"name": "青苔密卷", "used": false}, {"name": "锈蚀怀表", "used": true}], + "feats": ["疼痛记忆 ×2", "双管镐 ×1"]} var po = load("res://presentation/views/status_panel.gd").new() po.open_status(data) Kit.check("状态面板开层可见(open_status 直开——P4 甲零动效路径)", po.visible) - Kit.check("余额徽投影 = keystone_balance() 读数(%s)" % po._balance_badge.value_label.text, - String(po._balance_badge.value_label.text) == "钥石 %d" % mp.keystone_balance()) - var ws: Array = mp.treasure_shop_status() - var ws_ok: bool = po._workshop_holder.get_child_count() == ws.size() - var uikit_s = load("res://presentation/views/ui_kit.gd") - for i in mini(ws.size(), po._workshop_holder.get_child_count()): - var row: Dictionary = ws[i] - var lbls2: Array = [] - for ch in po._workshop_holder.get_child(i).get_children(): - if ch is Label: - lbls2.append(ch) - var unlocked: bool = bool(row.get("unlocked", false)) - var expect_prog := "已入手" - if not unlocked: - if String(row.get("mode", "")) == "keystone": - expect_prog = "钥石 %d" % int(row.get("price", 0)) - elif String(row.get("mode", "")) == "condition": - var parts: Array[String] = [] - for req_v2 in (row.get("reqs", []) as Array): - var req2: Dictionary = req_v2 as Dictionary - var lbl2 := String(uikit_s.TREASURE_REQ_LABELS.get(String(req2.get("key", "")), "条件")) - parts.append("%s %d/%d" % [lbl2, int(req2.get("progress", 0)), int(req2.get("threshold", 0))]) - expect_prog = "、".join(parts) - var expect_st2 := "已解锁" if unlocked else "未解锁" - ws_ok = ws_ok and lbls2.size() == 3 \ - and String(lbls2[0].text) == String(row.get("display_name", "")) \ - and String(lbls2[1].text) == expect_prog \ - and String(lbls2[2].text) == expect_st2 - Kit.check("秘宝匣 12 行投影 = treasure_shop_status() 逐项(名称/钥石价与条件进度/解锁态——首件解锁夹具在列)〔秘宝批 V19 呈现数据源〕", - ws_ok) - var owned := 0 - for trow in (mp.tree_camp_rows() as Array): - if int(trow.get("level", 0)) > 0: - owned += 1 - var tree_txt := String(po._tree_label.text) - Kit.check("天赋树概览聚合文案与已购节点数一致(零已购=空态句)", - tree_txt == "尚未购买任何节点(天赋树在营地主柜台)" or tree_txt.begins_with("已购 %d 项:" % owned)) + Kit.check("余额徽投影 = 快照 balance 读数(sv4 回归锚)〔sv4〕", + String(po._balance_badge.value_label.text) == "钥石 77") + Kit.check("属性行全值投影 = 契约4 钉值格式逐字(生命/上限/攻击/AC/GP 五现值)〔sv1〕", + String(po._attrs_label.text) == "生命 13/29 · 攻击 7 · 护甲 AC 11 · GP 41") + Kit.check("秘宝行投影 = 受理序 join+已用件「(已用)」尾缀〔sv2〕", + String(po._curios_label.text) == "青苔密卷 · 锈蚀怀表(已用)") + Kit.check("专长行投影 = 行串 join〔sv3〕", + String(po._feats_label.text) == "疼痛记忆 ×2 · 双管镐 ×1") + # 零旧段(sv5——跨关概览退役零残留:面板子树零旧文案+旧成员退役) + var stale := "" + for ch in _all_labels(po): + if String(ch.text).contains("道具工坊") or String(ch.text).contains("天赋树"): + stale += String(ch.text) + "|" + Kit.check("面板子树零「道具工坊/天赋树」文案(跨关概览退役零残留)〔sv5〕", stale == "") + Kit.check("旧段成员退役(_workshop_holder/_tree_label 零残留——契约2 数据/成员层面清零)〔sv5〕", + po.get("_workshop_holder") == null and po.get("_tree_label") == null) + # 空态句×2(curios/feats 空夹具各跑一遍——契约4 空态呈现) + var data_empty := {"balance": 3, "hero_hp": 5, "max_hp": 6, "atk": 2, "ac": 9, "gp": 1, + "curios": [], "feats": []} + var po2 = load("res://presentation/views/status_panel.gd").new() + po2.open_status(data_empty) + Kit.check("空秘宝态句=「未携带秘宝」〔sv2〕", String(po2._curios_label.text) == "未携带秘宝") + Kit.check("空专长态句=「未获专长」〔sv3〕", String(po2._feats_label.text) == "未获专长") + # 提示行文案(sv8——新定位换代;旧「永久强化进度」半句随两段退役失效) + var hint_txt := "" + for ch in _all_labels(po): + if String(ch.text).begins_with("查看本局"): + hint_txt = String(ch.text) + Kit.check("提示行=「查看本局冒险进度(只读)」(新定位换代)〔sv8〕", + hint_txt == "查看本局冒险进度(只读)") po.visible = false po.free() + po2.visible = false + po2.free() + + +# 面板子树 Label 收集(sv5 零旧段扫描辅助——递归全子树) +func _all_labels(root: Node) -> Array: + var out: Array = [] + if root is Label: + out.append(root) + for child in root.get_children(): + out.append_array(_all_labels(child)) + return out # ---- 生涯页只读投影一致性(汇总段六类九项逐行 == SaveManager 计数访问器/ diff --git a/game/tests/unit/presentation/ui_font_asset_test.gd b/game/tests/unit/presentation/ui_font_asset_test.gd index 5ade37d4..b4b6f1bb 100644 --- a/game/tests/unit/presentation/ui_font_asset_test.gd +++ b/game/tests/unit/presentation/ui_font_asset_test.gd @@ -17,6 +17,7 @@ func _init() -> void: if f != null: Kit.check("中文字形在集(攻/雷/旗/封)", f.has_char("攻".unicode_at(0)) and f.has_char("雷".unicode_at(0)) \ and f.has_char("旗".unicode_at(0)) and f.has_char("封".unicode_at(0))) + Kit.check("中文字形在集(危——已开危险格字形换代前置,迭代36 u12/wa9)", f.has_char("危".unicode_at(0))) Kit.check("ASCII/数字字形在集(A/0/%)", f.has_char("A".unicode_at(0)) and f.has_char("0".unicode_at(0)) and f.has_char("%".unicode_at(0))) var sz := 18 var w_cn: float = f.get_string_size("战斗胜利", HORIZONTAL_ALIGNMENT_LEFT, -1, sz).x diff --git a/reports/iter34-代拍板-2026-10-22.md b/reports/iter34-代拍板-2026-10-22.md deleted file mode 100644 index 324eaffe..00000000 --- a/reports/iter34-代拍板-2026-10-22.md +++ /dev/null @@ -1,33 +0,0 @@ -# 迭代 34 代拍板追认单(2026-10-22) - -> 本单只登记实现期在卡片 §10 白名单内所做的代拍项,属备案确认,不是事后授权。白名单依据=`reports/需求卡片-敌人表深度带换代批-2026-10-21.md` §10。 - -## 卡③ 敌人表深度带换代批 - -| # | 白名单条目 | 实际做法 | 约束核验 | -|---|---|---|---| -| 1 | 带切法实现形态与带键编号(§10-1) | 新增 `EnemyTable.band_of(band_keys, D)` 纯静态函数:对升序带键取首个 ≥ D 的键,全部小于 D 时返回 -1;resolver 侧 -1 → 取顶带+`resolve_depth_clamp` 告警(四元组去重沿用) | 迁移期带键=旧 `node_depth` 数值,请求逐位等价;单点收口(resolver 与跨表校验都调这一个函数);D<0 仍走 `request_oob` 致命通道 | -| 2 | 新字段/常量命名与枚举值(§10-2) | 表列名 `row_kind`/`depth_band`;常量 `EnemyTable.ROW_KINDS := ["battle", "boss"]`;枚举字符串与关卡节点 `type` 词表同字 | 与节点 type 词表一致;非法值由 `validate` 的 `domain` 类别拒绝 | -| 3 | `resolve_enemy_rowset` 第三参命名与缺省映射(§10-3) | 第三参命名 `row_kind`;`BattleOrchestrator` 由 `enc.node_type` 取值、缺省 `"battle"` | 生产路径 `level_runner` 恒传 `node_type`;缺省值只服务冒烟/单测直呼面 | -| 4 | 校验错误类别命名(§10-4) | 全部沿用既有类别:`domain`/`dup_key`/`rowset_rule`/`anchor`/`level_node_dangling` | 未新增类别;Boss 同带与顶带消解分别复用 `rowset_rule` 与 `level_node_dangling` | -| 5 | 冒烟断言的定位写法与措辞(§10-5) | 首遇位一律改为扫表定位:`_first_encounter_band(L, enemy_id)` 取该 (关卡, battle, 敌种) 的最低带;断言名带 fv 尾缀 | 首遇定位零写死坐标;写死坐标的旧断言全部移除 | -| 6 | 提交切分与分支命名(§10-6) | 分支 `iter34-enemy-band`;卡③分两笔:核心与数据面一笔、断言面一笔 | 两笔都在 `game/` 内,批次合并时按 diff 推代码域 | - -## 白名单外未代拍事项 - -无。实现期未遇到需要超出 §10 清单自决的设计分歧。 - -## 卡④ 信息迷雾可见性批 - -> 白名单依据=`reports/需求卡片-信息迷雾可见性批-2026-10-21.md` §9(六条)。 - -| # | 白名单条目 | 实际做法 | 约束核验 | -|---|---|---|---| -| 1 | provisional N=2(§9-1) | `level.tres` 挂 `info_visible_layers = 2`;`validate` 只校验 int ≥ 1 | 定值仍在数据表,代码零硬编码;N=1/3 改表实测可见窗随动 | -| 2 | API/探针命名(§9-2) | `info_visible_layers`/`type_visible_ids`/`node_display`/`display_probe` | 均无 `fog_` 前缀,与战斗「视野迷雾」零撞词 | -| 3 | 雾下节点呈现落点(§9-3) | 图标/类型文字改读 `node_display` 输出;节点方块、编号、连线结构通道零改动 | 编号保留、类型文字不绘制;结构通道冒烟与渲染闸全绿 | -| 4 | 大图夹具规模(§9-4) | 12 个 BFS 层构造(跨层跳边压层后布局实得 11 层×6 槽)、含双 Boss、跨层跳边 | 满足 ≥10 层 × ≥5 槽;断言按布局实测 `maxl/maxslot` 推导 | -| 5 | 常数锚同源化方式(§9-5) | 探针直接引 `LevelMapScreen.LAYER_BASE_X/LAYER_STEP_X/NODE_HALF` 导出常量 | 与 `_fit_canvas` 公式逐项同源,无手抄数字副本 | -| 6 | 提交切分与分支命名(§9-6) | 分支 `iter34-fog`;三笔:实现与断言(`ed8e0f9f`)、⑱ 判据改绑(`4ef154b9`)、类型闸归零+探针补尾缀(`6f04f29b`) | 三笔均在 `game/` 内;报告随批落 `reports/` | -| 7 | 白名单外——审计缺漏 D1 的接线层验收手段(卡片 §9 未列,已呈报三案) | 呈报甲(补绘制输出观测面)/乙(改 §7.1 口径)/丙(单次快照降级)并推荐甲;用户回「继续未完成的任务」后按甲落地:`_draw_graph` 绘制点记录本帧绘制事实,`display_probe()` 改读该记录 | 改动集中在提交 `1970e0ef`,一笔可回退;未触碰冻结视觉件(零新画法/色值/线宽/缩放滚动),新增面为只读成员与记录赋值 | - diff --git a/reports/iter34-卡④-验收审计-2026-10-22.md b/reports/iter34-卡④-验收审计-2026-10-22.md deleted file mode 100644 index eda8085d..00000000 --- a/reports/iter34-卡④-验收审计-2026-10-22.md +++ /dev/null @@ -1,164 +0,0 @@ -# 迭代 34 卡④(信息迷雾可见性批)独立验收审计报告(2026-10-22) - -> 审计员:未参与本卡实现的独立子代理。审计期只读源码/测试/产出物,自跑命令取证;除本报告外未写任何文件。 -> 审计对象:`D:\code\g-plan2-iter34-fog`(分支 `iter34-fog`,任务指定被审 HEAD `477fd0f5`,审计起点 `43c0f3c1`)。 -> 审计期分支前进说明:审计过程中分支新增 `6757c21e`(docs-only,仅 `reports/iter34-反哺点-2026-10-22.md` 追加 ⑮ 体验验证清单登记,6 行)。本报告结论按任务指定 HEAD `477fd0f5` 的代码面给出;`6757c21e` 不触及代码与测试,不影响任何判定,仅在反哺点相符性一节并入核对。 - -## 1. 审计范围与基线 - -| 项 | 内容 | -|---|---| -| 被审 diff | `git diff 43c0f3c1..HEAD`:14 个文件(`game/` 11 件:核心 1/schema 1/数据 1/呈现 1/冒烟 2/单测 4(含 2 个 .uid)/manifest 1;`reports/` 3 件)。审计期 HEAD 前进后为 15 件(多 1 件 docs-only) | -| 工作树状态 | 审计开工与收尾 `git status --short` 均为空;4 次变异注入后均 `git checkout --` 还原并复核为空 | -| 环境 | Godot 4.7.2 console;python 前置 `PYTHONIOENCODING=utf-8`;长测前清理了残留 `ui_click_probe` Godot 进程(2 个),全程串行单实例 | -| 不采信项 | 实现报告 §5.7 的「已全绿」自述仅作对照,判定一律用下面自跑读数 | - -### 1.1 自跑命令与读数(原文) - -| # | 命令(工作目录) | 读数原文 | 退出码 | -|---|---|---|---| -| C1 | `Godot…console.exe --headless --path . --script res://tests/unit/core/level_map_type_vis_test.gd`(`game/`) | `[level_map_type_vis_test] all green (26 checks)` | 0 | -| C2 | 同上换 `res://tests/unit/presentation/level_map_info_fog_test.gd` | `[level_map_info_fog_test] all green (19 checks)` | 0 | -| C3 | 同上 `res://tests/run_all.gd` | `[run_all] all green (40 suites)` | 0 | -| C4 | `Godot…console.exe --headless --path . res://smoke/ui_input_probe.tscn`(`game/`) | `[ui_input_probe] PASS — …十八断言全绿`(见 §4 观察项 3) | 0 | -| C5 | `python tools/run_smoke.py`(工作树根) | `[run_smoke] 摘要: 冒烟完成:1211 项检查,0 项失败`+`[run_smoke] PASS(退出码 / 摘要失败数 / 脚本错误扫描 三闸全过)`;新增 5 条「可见性冒烟」断言逐条 `[PASS]`(含〔fv4〕〔fv3〕〔fv6〕〔fv1〕) | 0 | -| C6 | `bash game/tests/static_scan.sh` | `[audit_type_hints] PASS——产品码类型闸归零(R1/R2/R3)`;`[static_scan] S9 ok` … `结构审计 PASS` | 0 | -| C7 | `python tools/audit_type_hints.py` | `[audit_type_hints] PASS——产品码类型闸归零(R1/R2/R3)` | 0 | -| C8 | 读 `game/tests/.gates/receipt.json` | `commit: 4ef154b9…`;`09 静态扫描 exit: "1"`;`all_green: false`;`changed_vs_baseline: ["09 静态扫描:红项清单变化…"]` | — | - -要点:C1–C7 证明**当前代码面**的冒烟、单元、探针、静态扫描在我手上全绿;C8 证明**闸回执证据**停留在 `4ef154b9`,且该次 09 红、`all_green=false`,此后还有 2 个代码相关提交(`6f04f29b` 改 3 个 `game/` 文件、`477fd0f5`/`6757c21e` 仅文档)。这一差异见 §5.2 与 §6 缺漏 D2。 - -## 2. §7.1 对照表逐行结论(fv1–fv9) - -判定口径:①断言真实存在且尾缀可 grep;②真实入口符合卡片该行声明;③断言强度足以在改动撤销/单点变异时变红。行内「通过」均附自跑依据。 - -| fv | 断言存在(文件:行,实测 grep) | 真实入口核对 | 断言强度核对 | 结论 | -|---|---|---|---|---| -| **fv1 邻近 N 层见类型** | `level_map_type_vis_test.gd:121/122/123`(层距 2 可见、3/4 不可见)、`:142/143`(入口锚层 0)、`:166/167/168`(单链退化);`smoke_all.gd:4942-4943`(真图节点 7/8 入雾)。尾缀〔fv1〕共 8 处断言+3 处注释 | 核心 `type_visible_ids()` 直调(卡片该行声明即此)+冒烟真图 L2 入口停点 | 边界两侧都断言(N 内真、N+1 假),非恒真;M1 实测 4 条红(§3) | **通过** | -| **fv2 范围外只见结构** | `level_map_info_fog_test.gd:51/54/56`(显隐与无描边)、`:96/98/100`(label 三态);`ui_input_probe.gd:59-62`(⑱ 雾下节点双通道,尾缀在 :60)。尾缀〔fv2〕共 8 处 | 单元=`ScreenScript.node_display` 纯函数(卡片允许);探针=`ro.display_probe()`。**但 `display_probe()` 内部是逐节点重调 `node_display`(`level_map_screen.gd:536-547`),不是读取实际绘制输出**——卡片该行要求「真机 `display_probe()` 读绘制面(接线层验收——防『断言在、接线坏、不红』)」不成立 | 单元决策面断言有效(M2 红);⑱ 的「图标不出示」判据自指(fog_id 正是按 `draw_icon==false` 选出,再断言其 false,恒真;「文字不出示」判据有效)。**接线层(`_draw_graph` 是否消费决策面)零断言:我注入「两处 `if bool(disp[...])` 直接改 `if true`」=模拟接线坏,C2 仍 `19 checks` 全绿、C4 仍 PASS**(§3 W1) | **缺漏**(详见 §6 D1) | -| **fv3 Boss/终点不入雾+多终点标记** | `level_map_type_vis_test.gd:144/145`(远距 Boss 在集、同层距非 Boss 不在集);`level_map_info_fog_test.gd:65/67/70/73`(双 Boss 描边、Boss 三通道在、远 Boss 不入雾、豁免不扩散);`ui_input_probe.gd:63-64`;`smoke_all.gd:4939`(真图 L2 Boss=9 在集)。尾缀〔fv3〕共 9 处断言 | 核心可见集直调+`node_display` 直调+真机 `display_probe`(卡片该行声明即此)+冒烟真图 | 单 Boss 核心豁免与双 Boss 决策面均有断言;M3/M4 报告已证(我未复跑,M2 连带 `Boss 豁免不扩散〔fv3〕` 实测红,说明该条有敏感度)。观察项:双 Boss 夹具把 7/9 直接写进 `type_vis=[1,7,9]`,未从 `type_visible_ids()` 推导「均在集」,多终点核心成员性只有单 Boss 用例覆盖同一代码分支(非阻塞,见 §4 观察项 4) | **通过**(含 1 条观察项) | -| **fv4 下一跳恒可见** | `level_map_type_vis_test.gd:132/133`(跨层跳边候选层距 N+1 仍在集、同锚非候选不在集);`smoke_all.gd:4938`(真图候选 ⊆ 可见集);`ui_input_probe.gd:66-67`(真机候选 draw_icon)。尾缀〔fv4〕共 3 处 | 核心直调+冒烟真图+探针 | 跨层跳边是反例夹具,删候选豁免会红(报告 M5;未复跑);M2 不连带此条,逻辑独立 | **通过** | -| **fv5 节点内详情不显示** | `level_map_info_fog_test.gd:87/88`(携带 `enemy_id/reward_id` 与不带者逐键相等;label 不含内容值,实际打印「战斗 · 可选」)。尾缀〔fv5〕共 3 处 | `node_display` 纯函数(卡片该行声明即此) | 逐键相等+字符串不含,两条互补;M6 报告已证(未复跑)。`node_display` 正文只读 `type`,代码审读一致 | **通过** | -| **fv6 N 进数据表** | `level_map_type_vis_test.gd:96`(真表读数 2)、`:100/102/104`(N=1 合法、0/-1 判 domain 错)、`:178/180/186`(改表 N=1/3/10 可见窗随动);`smoke_all.gd:4940-4941`(真图读表=2)。尾缀〔fv6〕共 7 处断言 | `LevelTable.validate()`+`db.level.info_visible_layers` 读值(核心直调,卡片声明即此) | 不是「读一次字段」——改表值三档窗随动+越域双值判错,强度够;M7a/M7b 报告已证(未复跑) | **通过** | -| **fv7 单圈语义** | `level_map_type_vis_test.gd:155/156/157`(visited 不豁免:远/中距回雾、近端仍显)、`:207`(`start_level_external` resume_state 重建与直入逐位一致)。尾缀〔fv7〕共 6 处 | 核心直调+真实 resume 入口 | visited 显式注入后仍不可见=单圈成立;resume 断言两侧同函数、若两者同空会平凡相等,但同套件其余组已证函数非空输出,风险可接受 | **通过** | -| **fv8 画布自适应** | `level_map_info_fog_test.gd:169`(规模实测 maxl=10/maxslot=6)、`:175`(min.x 1334.5 ≥ 1334.5)、`:177`(min.y 449.0 ≥ 449.0)、`:185`(全节点方块在画布内)、`:192/195`(双 Boss 描边、远处战斗 32 入雾);`ui_input_probe.gd:146-149`(常数锚引屏内常量)+⑬ | `canvas_probe()`/`layout_probe()`+真机探针 ⑬。⑬ 的 BFS 独立推导、常数同源 | 公式两侧独立推导(布局实测反解 maxl/maxslot,非用 `_fit_canvas` 自己的层表);M9 实测 2 条红(§3)。观察项:`layout_probe` 的 `maxl` 由 `_content_size` 居中后的坐标反解,若宽度公式少项,反解仍正确——正是 M9 能红的原因,无缺陷 | **通过** | -| **fv9 显隐接线(雾不扩可点域)** | `ui_input_probe.gd:76-91`(真机 `push_input` 点击雾下非候选节点,`route_chosen` 零发射,尾缀 :89) | 真机 push_input+真实 `_candidate_at` 命中面;判据绑 `route_chosen` 信号面(可观测提交面),未绑兜底终态 | M10 实测红,原文 `提交=[3] visible=true 选路态=true`(§3);这是本卡最强的一条 | **通过** | - -**§7.1 汇总**:fv1、fv3、fv4、fv5、fv6、fv7、fv8、fv9 通过(fv3 含 1 条观察项);**fv2 判缺漏**——不是「断言不存在」,而是卡片该行声明的「读绘制面/接线层验收」用当前 `display_probe()` 实现不成立,且实测接线坏不红。 - -## 3. 变异复现记录(自跑) - -四张 T 卡各复现一条(T1→M1、T2→M2、T3→M9、T4→M10),全部「注入→最窄闸见红→`git checkout --` 还原→复跑绿」,另加一条审计自设的接线坏注入 W1。**无复现不一致**:四条红的位置与报告 §5.4 所写逐字一致。 - -| # | T 卡 | 注入内容(精确位置) | 红断言原文(自跑) | 还原后读数 | -|---|---|---|---|---| -| **M1** | T1 | `game/core/level_runner.gd:433`:`absi(...) <= n` → `< n` | `[FAIL] 层距 N 内可见:锚层 1 取节点 4(层距 2)〔fv1〕`;`[FAIL] 单圈只按锚层取窗:已过近端节点 4(层距 2)仍可见〔fv7〕`;`[FAIL] 单链退化形态:入口锚层 0,下标 2 可见〔fv1〕`;`[FAIL] N 读表:N=3 时节点 5(层距 3)入窗〔fv6〕`;`4/26 项失败`,退出码 1 | `git status --short` 空;C1 复跑 `all green (26 checks)` 退出码 0 | -| **M2** | T2 | `game/presentation/views/level_map_screen.gd` `node_display`:`"draw_icon": type_shown` → `true` | `[FAIL] 雾下节点:图标与类型文字均不绘制(显隐语义,非灰化)〔fv2〕`;`[FAIL] Boss 豁免不扩散:雾下非 Boss 仍隐类型〔fv3〕`;`[FAIL] 大图呈现:入口停点远处战斗节点(编号 32,层 6)类型入雾〔fv8〕`;`3/19 项失败`,退出码 1 | `git status --short` 空;C2 复跑 `all green (19 checks)` 退出码 0 | -| **M9** | T3 | `level_map_screen.gd:_fit_canvas` 宽公式去 `+ NODE_HALF.x`(`LAYER_BASE_X + maxl*LAYER_STEP_X + 8.0`) | `[FAIL] 大图拟合:画布 min.x 覆盖最深节点(1298.0 ≥ 1334.5)〔fv8〕`;`[FAIL] 大图布局:全节点方块落在拟合画布内(origin=(0.0,0.0) size=(1298.0,449.0))〔fv8〕`;`2/19 项失败`,退出码 1 | `git status --short` 空;C2 复跑 `all green (19 checks)` 退出码 0 | -| **M10** | T4 | `level_map_screen.gd:_candidate_at`:`for nid in _cand_ids` 改为遍历全节点 id(先构 `_all_ids`) | `[ui_input_probe] FAIL — case⑱ 雾下节点点击产生提交(提交=[3] visible=true 选路态=true——雾不扩大可点域)〔fv9〕`,退出码 1 | `git status --short` 空;C4 复跑 `PASS` 退出码 0 | -| **W1**(审计自设,报告未列) | T2 接线层 | `_draw_graph` 两处条件 `if bool(disp["draw_icon"]):`/`if bool(disp["draw_label"]):` 均改 `if true:`(模拟「决策面正确、接线坏」) | **不红**:C2 `all green (19 checks)` 退出码 0;C4 `PASS` 退出码 0 | `git status --short` 空;C2 复跑 `all green (19 checks)` 退出码 0 | - -W1 的结论:本卡唯一「防止接线坏」的声明(§7.1 fv2 真实入口格)在端到端两层闸上都不成立——这正是卡片要求「真机 `display_probe()` 读绘制面」的动机,而 `display_probe()` 只是决策面的第二次求值。 - -## 4. 测试削弱检查结论 - -| 检查面 | 结论 | -|---|---| -| 删除既有断言 | **无**。`git diff 43c0f3c1..HEAD -- game/smoke game/tests` 的全部删除行只有 3 行,均为 `ui_input_probe.gd` 的画布常数锚注释+`need_x2` 手抄公式行,属卡片 §7.1「旧断言预声明」第①类允许项(改为引 `LevelMapScreen.LAYER_BASE_X/LAYER_STEP_X/NODE_HALF`,判据与断言语义未变)。其余冒烟/探针文件只增不删 | -| 放宽阈值 | **无**。冒烟新增 5 条全为新判据;⑬ 容差 `-0.5` 未动;⑱ 无「非空即过」类宽松判据 | -| 缩小扫描范围 | **无**。新两套件扫描全节点;大图夹具 63 节点全量比对 | -| 真实入口换下层直调 | **fv2 一项成立**(§2、§6 D1):`display_probe` 是决策面重算,不是绘制面读取。其余各行入口与卡片声明一致 | -| 注释掉断言 | **无** | -| 越出卡片声明的削弱 | **无**(除 fv2 的入口口径问题外) | - -观察项(非削弱,但影响判据强度,建议回炉 T2 时一并处理): -1. ⑱ 的「雾下图标不出示」判据自指:fog_id 按 `not disp[vnid]["draw_icon"]` 选出,再断言 `not draw_icon`,该半句恒真;有效的是「文字通道不出示」半句与「雾面节点存在」的存在性(M2 时因选不到 fog 节点而走 `case⑱ 雾面节点选取失败` 分支变红)。建议改为选区与断言解耦(如按层距/候选集独立选雾节点)。 -2. fv3 双 Boss 夹具的 `type_vis` 是手写 `[1,7,9]`,只证「决策面对两个 Boss 都亮」,未从 `type_visible_ids()` 推导「两端点均在集」;核心多 Boss 成员性由单 Boss 用例覆盖同一分支,风险低。 -3. C4 的 PASS 文案仍列到 ⑰ 为止(「十八断言」按 18 个 case 数),未点名 ⑱;报告 §5.2 称「探针⑱ 5 项断言」,实现实为 4 个 `_pre_fail` 判据+1 个选区前置。均为措辞/计数瑕疵,不影响判据有效性。 - -## 5. 报告 / 代拍板 / 反哺点相符性 - -### 5.1 实现报告 §5.3 六条偏差逐条核对 - -| 偏差 | 核对结果 | -|---|---| -| 1 大图夹具 12 BFS 层→布局 11 层(实测 maxl=10/maxslot=6) | **如实**(C2 打印「实测最大层 10/最大槽 6」;夹具源码 `tiers := 12`+跨层跳边) | -| 2 ⑱ 零提交判据改绑 `route_chosen` | **如实**(M10 红在信号面;旧终态判据被屏态/运行器兜底吸收的解释与 M10 输出 `visible=true 选路态=true` 一致)。属卡片 §8 L1 写法自救,已申报 | -| 3 类型闸 4 处 R1(`6f04f29b` 归零) | **如实**(旧 `static_scan.log` 逐行列 4 处 R1;C6/C7 现为 PASS) | -| 4 常数锚行号 `:98-99` → `:142-145` | **如实**(现文件 `:146-149` 为注释+公式;卡片原行号已 T0 订正) | -| 5 画布下限 620×256 vs 冻结文档 620×240 | **如实**(`level_map_screen.gd:303`=`Vector2(620, 256)`;`design/gdd/ux/level-map.md:68` 写 620×240;反哺点 ⑧ 已登记) | -| 6 命中面 `_candidate_at` 零改动 | **如实**(diff 该函数零 hunk;M10 是我注入才改) | - -**未申报的偏差 1 条**:§5.2 fv2 行写「真机 `display_probe()` 读绘制面」,实际 `display_probe()` 只是 `node_display` 快照重算,`_draw_graph` 接线零断言(W1 证明)。报告 §5.3 未列此偏差。 - -**报告 §5.7 与回执不符 1 条**:报告表 09 行写「1 → 0」,读作最终已绿;但 `receipt.json` 的 `commit=4ef154b9`、`09 exit=1`、`all_green=false`,且该回执早于 `6f04f29b`(修 09 的提交)与分支尖。报告末句「最终合并回执见本批合并提交信息(起跑 commit=分支尖)」是未来义务,不是已落证据;而 §5.1 已把 T4 标「完成」。**「批末代码域闸全绿」目前只有窄验证(audit_type_hints PASS+两套件+探针),没有分支尖的 `run_all_gates.sh --code` 回执。** - -(补充事实:C8 的 `changed_vs_baseline` 是「09 红项清单变化」。存量基线 `baseline.json` 里 09 本就是 exit=1 的存量红;本批先新增 4 处 R1 被判「清单变化」,修复后应转绿。所以问题不在代码现状,而在回执证据未随修复重生成。) - -### 5.2 代拍板六条(卡④)白名单核对 - -| # | 白名单条目 | 核对结果 | -|---|---|---| -| 1 | provisional N=2 | **合规**:`level.tres` 挂 2;`validate` 只校验 int≥1;代码零硬编码 N(`level_runner.gd:429` 读 `db.level.info_visible_layers`);C1 证明改表值窗随动 | -| 2 | API/探针命名 | **合规**:`info_visible_layers`/`type_visible_ids`/`node_display`/`display_probe` 均无 `fog_` 前缀。`level_map_info_fog_test.gd` 及 `_test_fog_hides_type` 属卡片 T2 指定的测试文件名/测试内私有函数,不是跨层 API | -| 3 | 雾下呈现落点 | **合规**:编号、方块、连线绘制零改动(生产 diff 删除行只有图标/文字块重排与条件切换,色值/线宽/形制零变);C5 冒烟 1211 项、C6 渲染相关静态面全绿 | -| 4 | 大图夹具规模 | **合规**:12 BFS 层压成 11 层×6 槽、含双 Boss、含跨层跳边(1→3 层目标),满足 ≥10×≥5 | -| 5 | 常数锚同源化 | **合规**:探针直接引 `LevelMapScreen` 导出常量,无手抄副本 | -| 6 | 提交切分/分支名 | **合规**:`iter34-fog`,三笔均在 `game/`;报告/产出物在 `reports/` | - -白名单外自决:**未发现**(W1 揭示的是验收口径缺失,不是实现自决)。 - -### 5.3 反哺点 ⑥–⑮ 对卡片 §5 的覆盖 - -| 卡片 §5 同步面/呈报项 | 反哺点 | 结论 | -|---|---|---| -| 关卡节点地图(N 状态行+可见性语义) | ⑥+⑩ | 覆盖 | -| 关卡节点图生成(屏预算行+下一跳兑现标注) | ⑦ | 覆盖 | -| level-map(画布公式漂移+Information Hierarchy+**雾的正式视觉规格呈报项**) | ⑧(含呈报项,明确「禁本批落文」) | 覆盖 | -| 术语表(信息迷雾 vs 视野迷雾) | ⑨ | 覆盖 | -| 待办清单 | ④+⑪ | 覆盖 | -| roadmap | ⑤+⑫ | 覆盖 | -| **超预算图升级路径(呈报项)** | ⑬ | 覆盖 | -| 实机目检三项登记 | ⑮(审计期新增提交 `6757c21e`) | 覆盖 | -| 方法论反哺 | ⑭ | 额外,合理 | - -### 5.4 契约与代码纪律 - -| 检查 | 结论 | -|---|---| -| 契约 1/2(三层语义、Boss/终点/候选豁免) | 未破:结构通道全绘;类型由 `type_vis` 决定;Boss 在核心与决策面双豁免;候选追加进核心集 | -| 契约 3(N 只进数据表) | 未破:唯一字面量 2 是 `level_table.gd:8` 的 schema 默认值(卡片 T1 明确要求),运行期经 `db.level` 读 | -| 契约 4(显隐、零新画法) | 未破:无灰化/雾纹理/新色值/线宽变化 | -| 契约 5(单圈零记忆) | 未破:实现不读 `visited`;C1 单圈组绿 | -| 契约 6(不动冻结件/禁缩放滚动) | 未破:`_fit_canvas` 公式零改动;下限 620×256 是改前既有值(卡片 §5.3-5) | -| 契约 7(呈现只读) | 未破:呈现层不重算层距,只消费 `type_visible_ids()`;`node_display` 的 Boss 判定属卡片 T2 指定的决策面语义 | -| 契约 8 / §8 L3 | 未破:无生成器/行进/敌人表/校验放宽改动;`type_visible_ids` 无状态;候选未被雾遮 | -| core 零 UI 依赖 | 通过:`level_runner.gd` 仅注释提及 `LevelMapScreen`,无 UI 类型引用 | -| 数值零字面量 / RNG | 通过:N 读表;新代码无 `randomize()`、无自建 RNG | -| 行尾/类型闸 | 通过:核心 `.gd` 无 CRLF;`audit_type_hints` PASS(C7) | -| 头注/变更记录禁令 | 通过:新增文件头只述职责;无版本历史节;`git diff --check` 仅 3 个报告文件「EOF 空行」提示,非代码问题 | - -## 6. 总判定:**不通过** - -代码现状的自跑读数全部为绿(C1–C7),未发现测试削弱、契约破坏、越权自决或数值/RNG 违规。但按卡片 §7.1 与 T4/T5 的验收口径,有 **2 条缺漏**必须回炉后重审: - -### D1(回炉 T2)fv2「接线层验收」未成立,且卡片 T2 与 §7.1 口径冲突 - -- **缺什么**:证明 `_draw_graph` 真的消费 `node_display` 输出的断言。当前 `display_probe()`(`level_map_screen.gd:536-547`)逐节点重新调用 `node_display`,与绘制路径是两次独立求值;测试与探针都只读这个快照。 -- **在哪**:`level_map_info_fog_test.gd:187-195`(大图 `display_probe`)、`ui_input_probe.gd:46-68`(⑱ 前四段);证据=W1 注入(`_draw_graph` 两处条件改 `if true`)后 C2 `19 checks` 全绿、C4 PASS。 -- **口径冲突**:卡片 T2 把 `display_probe()` 定义为「id → `node_display` 快照」,而 §7.1 fv2 要求它「读绘制面(接线层验收)」。实现按 T2 落地,§7.1 判据不可能由该接口兑现。这属 spec-AC 不一致,需用户裁:要么 T2 改接口/补一层「绘制输出可观测」的探针(例如自绘记录器:给 canvas 包一层记录 draw_rect/draw_texture_rect/draw_string 的测试桩,或离屏渲染取雾节点区域像素),要么由用户修改 §7.1 fv2 的验收口径(实现批不得自决)。无论走哪条,当前状态下 fv2 的撤销证明只覆盖决策函数,不覆盖接线。 -- **附带**:⑱ 的雾下图标判据自指(§4 观察项 1),回炉时一并解除自指。 - -### D2(回炉 T4/T5)批末代码域闸无分支尖绿回执 - -- **缺什么**:与分支尖同 commit 的 `tools/run_all_gates.sh --code` 全绿回执。 -- **在哪**:`game/tests/.gates/receipt.json`——`commit=4ef154b9`、`09 静态扫描 exit="1"`、`all_green=false`;`6f04f29b`(修 09)与 `477fd0f5` 之后未重跑。实现报告 §5.1 却把 T4 标「完成」、§5.7 表写「09 1 → 0」。报告 §5.6 自己也写明「合并前回执须与分支尖同 commit」。 -- **处置**:在分支尖重跑 `tools/run_all_gates.sh --code` 落新回执(`commit` 字段=分支尖、`all_green: true`),再交复审;不得用窄验证替代。我的 C1–C7 已证明重跑应能绿,但回执是合并闸的强制证据,不能由审计代跑(会覆写既有闸产物)。 - -### 非阻塞观察项(不单独构成缺漏,建议随 D1/D2 一并处理) - -1. ⑱ 雾下图标判据自指(§4-1)。 -2. fv3 双 Boss 夹具手写 `type_vis`,多终点核心成员性无独立推导(§4-2)。 -3. 探针 PASS 文案未列 ⑱,报告「5 项断言」与 4 个判据不符(§4-3)。 - -**合并前置**:D1、D2 关闭并复审通过前,本卡不得合并(卡片 T5、§7.1 末段)。 diff --git a/reports/iter34-卡④-验收审计-复审-2026-10-22.md b/reports/iter34-卡④-验收审计-复审-2026-10-22.md deleted file mode 100644 index b73a4e28..00000000 --- a/reports/iter34-卡④-验收审计-复审-2026-10-22.md +++ /dev/null @@ -1,75 +0,0 @@ -# 迭代 34 卡④(信息迷雾可见性批)独立验收审计·第二轮复审报告(2026-10-22) - -> 审计员:第二轮独立复审子代理,未参与实现、未参与第一轮审计。职责=复核甲案是否真关掉 D1、且无新问题。 -> 全程只读源码/测试/产出物+自跑最窄命令取证;除本报告外未写任何文件(注入变异均在跑完后 `git checkout --` 还原)。 -> 被审对象:`D:\code\g-plan2-iter34-fog`,分支 `iter34-fog`,被审 HEAD `de9cc102`(工作树开工/收尾 `git status --short` 均空)。 -> 被审改动:甲案两笔——`1970e0ef`(代码:`level_map_screen.gd`+`ui_input_probe.gd` 两文件)、`de9cc102`(账面:仅 `reports/` 三件)。审计起点=第一轮报告 `reports/iter34-卡④-验收审计-2026-10-22.md`(D1/D2 两条缺漏)。 -> 口径:不采信实现报告 §5/§六 的「已全绿/已闭合」自述,凡结论必附自跑读数或文件:行。 - -## 1. 自跑命令与读数原文 - -| # | 命令(工作目录=该工作树 `game/`) | 读数原文 | 退出码 | -|---|---|---|---| -| R1 | `…console.exe --headless --path . --script res://tests/unit/core/level_map_type_vis_test.gd` | `[level_map_type_vis_test] all green (26 checks)` | 0 | -| R2 | `…--script res://tests/unit/presentation/level_map_info_fog_test.gd` | `[level_map_info_fog_test] all green (19 checks)` | 0 | -| R3 | `…console.exe --headless --path . res://smoke/ui_input_probe.tscn` | `[ui_input_probe] PASS — …⑰未开格类型微光…⑱信息迷雾绘制接线(display_probe 读实际绘制记录)增)` | 0 | -| R4 | `…--script res://tests/run_all.gd` | `[run_all] unit/contract suite entry — 40 suites`;`[run_all] all green (40 suites)` | 0 | -| R5 | `PYTHONIOENCODING=utf-8 python tools/run_smoke.py` | `[run_smoke] 摘要: 冒烟完成:1211 项检查,0 项失败`;`[run_smoke] PASS(退出码 / 摘要失败数 / 脚本错误扫描 三闸全过)`;可见性断言逐条 `[PASS]`(含〔fv6〕N 读表=2、〔fv1〕层距 3/4 节点 7/8 入雾) | 0 | - -> 说明:R4/R5 只读验证单元/冒烟计数(run_all.gd 只写 `.gates/run_all_NN.log`,run_smoke.py 不写 `.gates`),**均未触碰 `receipt.json`**;未跑 `run_all_gates.sh`。R3 的 PASS 文案已点名 ⑱(第一轮观察项 3 已修)。 - -## 2. 四项逐条结论 - -### 项 1:D1 是否真关——**通过** - -- `display_probe()`(`level_map_screen.gd:546-547`)现只 `return _drawn_disp`,**不再自行调用 `node_display`**(全仓 `_drawn_disp` 仅 4 处:声明 :51、逐帧 clear :444、绘制点赋值 :517、返回 :547——无第二求值面)。 -- `_draw_graph` 在**实际绘制点**写记录:`boss_border`(:488,在描边画完之后)、`draw_icon`(:495,在 `if bool(disp["draw_icon"]):` 块内)、`draw_label`+`label_text`(:512-513,在 `if bool(disp["draw_label"]):` 块内),末 `_drawn_disp[nid] = rec`(:517)。记录随每帧 clear 重建(:444),只反映实际发生的绘制。 - -**W1 复现(第一轮判「不红」的那一发)**——把 :494/:511 两处 `if bool(disp["draw_icon"]|["draw_label"]):` 均改 `if true:`(`git diff` 净 2 行、无其它改动): - -- 呈现单元套件红:`ERROR: [FAIL] 大图呈现:入口停点远处战斗节点(编号 32,层 6)类型入雾〔fv8〕`(`level_map_info_fog_test.gd:195`)+`[level_map_info_fog_test] 1/19 项失败`,退出码 **1**。 -- 输入探针红:`[ui_input_probe] FAIL — case⑱ 雾下节点类型仍出示(图标=true 文字=true——范围外只见结构)〔fv2〕`,退出码 **1**。 -- `git checkout -- game/presentation/views/level_map_screen.gd` → `git status --short` 空 → 复跑 R2 `all green (19 checks)` 退出码 0。 - -**第一轮 W1=两入口全绿;本次两入口全红**——D1 关掉。**且两红原文与实现报告 §5.4 M11 行逐字一致**(§5.2 fv2 行「M11 绘制处绕开决策面 → 单元与探针双红(实测)」相符)。 - -**第二发新变异(验记录面不是摆设)**——绘制处条件保持原样(雾下节点实际不画图标),仅在 `_drawn_disp[nid] = rec` 前插一行无条件 `rec["draw_icon"] = true`(记录谎称画了图标、实际没画): - -- 呈现单元红:同〔fv8〕:195 一条,退出码 1;探针红:`case⑱ 雾下节点类型仍出示(图标=true 文字=false——范围外只见结构)〔fv2〕`(文字通道未被污染故=false,证断言确实逐值读记录),退出码 1。 -- 还原后 R2 `all green (19 checks)`。→ 记录的值是承重的(非仅存在性检查),断言确实消费绘制记录内容。 - -### 项 2:⑱ 自指是否解除——**通过** - -`ui_input_probe.gd` case⑱(:44-73)现况: - -- 选区=`fog_id`:`vnid != cur_id and not cands.has(vnid) and not vis.has(vnid)`(:56)——只读核心面 `vis = runner.type_visible_ids()`(:47)、`cur_id`(:48)、`cands`(:43),**不读 `disp`**(源码注释 :57 亦写明「选区只按核心可见集判,不读绘制记录」)。 -- 断言=`if bool(disp[fog_id]["draw_icon"]) or bool(disp[fog_id]["draw_label"])`(:64)——读呈现绘制记录。 -- Boss 选区按 `type=="boss"` 读节点数据(:54)、候选选区按 `cands[0]`(核心),断言分别读 `disp[boss_id]["boss_border"]`(:68)与 `disp[cands[0]]["draw_icon"]`(:71)。 - -选区全部来自核心/节点数据面,断言全部来自呈现绘制记录面——**两个不同面**,非第一轮「按 `not disp[...]["draw_icon"]` 选、再断言其 `not draw_icon`」的恒真自指。W1 变异能红即实证该断言不再恒真。→ 自指解除。 - -### 项 3:有无新削弱或新越权——**通过** - -- **删断言/放宽阈值/缩小扫描面/真实入口降级**:`git diff 43c0f3c1..HEAD -- game/` 的删除非注释行仅两类——① `level_map_screen.gd` 原「无条件绘制图标/字形/类型文字」块(改为经 `node_display` 决策面消费+写记录,属卡片 T2 本体,非删断言);② `ui_input_probe.gd` 旧手抄公式 `need_x2 = 70.0 + maxl2*122.0 + 26.0 + 8.0` 与旧 PASS 文案行。**无一条断言被删**。旧 `26.0`→新 `LevelMapScreen.NODE_HALF.x`(36.5,:153-154),阈值抬高 10.5px=**变严**(第一轮 F11 记探针低报仍绿,本次同源化后更紧),容差 `-0.5` 未动(:156)。真实入口 fv2 由「决策面快照」升为「绘制记录」=变强不变弱。 -- **`_drawn_disp` 是否只读观测面**:全仓仅 `level_map_screen.gd` 内 4 处引用(见项 1),**无新 `draw_rect/draw_texture_rect/draw_string`**、无色值/线宽/缩放滚动改动、不写存档、不引入持久状态语义;`clear`+记录赋值=纯只读观测面(仿 `canvas_probe`/`layout_probe` 先例)。甲案两文件之外零触达 core/data/smoke_all。 -- **早期返回空记录会否致平凡通过**:`_draw_graph` 早返回(`_runner == null or not visible`,:442)时记录不清空不重建。已核**无平凡通过面**——① 呈现单元 :192 先断言 `disp.has(boss_ids[0]) and disp.has(boss_ids[1])`,空记录即判红(且 :195 对缺失键 `disp[32]` 取 null 会报错、同样不放过),当前 19 项全绿反证记录已填充;② 探针 :59 前置判据 `not disp.has(fog_id) or not disp.has(boss_id) or not disp.has(int(cands[0]))` → `_pre_fail`(甲案新增),空记录走「记录缺位」红分支。两入口都对空记录设了拦截。 -- **代拍板第 7 行相符性**:卡片 §9 六条白名单(:216-221)无一条覆盖「把 `display_probe` 从 `node_display` 快照改为绘制记录观测面」这一接线层验收手段,属白名单外;代拍板第 7 行(`iter34-代拍板-2026-10-22.md:32`)如实备案「呈报甲/乙/丙三案并推荐甲,按甲落地:`_draw_graph` 绘制点记录本帧事实、`display_probe()` 改读该记录」,约束核验「改动集中 `1970e0ef` 一笔可回退、未触冻结视觉件、新增面为只读成员与记录赋值」——与代码实际做法、与本复审实测逐项一致。非静默越权。 - -### 项 4:D2 与账面相符——**通过** - -- `receipt.json`:`commit = de9cc102`=HEAD(分支尖)✓;`all_green = true` ✓;`changed_vs_baseline = []` ✓;闸项 01/02/03/04/05/09/10 全 `exit 0 / green`(09 静态扫描由「基线红→现绿·改善」,见 `inherited_red`);11 对拍按档位规则 NEED-APPROVAL(数值零变更,不发起,合规)。 -- 计数由自跑独立佐证:02 单元 **40 套件**(R4 `all green (40 suites)`)、01 冒烟 **1211 项 0 失败**(R5 摘要原文)——与回执 01/02 的 `exit 0` 相互印证。 -- 实现报告:§5.1 提交列表含 `1970e0ef`(:159「审计 D1 回炉 T2」)✓;§5.2 fv2 行与 §5.4 M11 行的红断言原文与项 1 实测逐字对得上 ✓;反哺点 ⑯(:100-104)写清「卡片 T2 :139 接口描述=`id → node_display 快照`滞后,与 §7.1 fv2『读绘制面』矛盾,须由**写卡段订正**、实现期不改卡片」✓。 - -## 3. 第一轮两条缺漏的闭合判定 - -- **D1(fv2 接线层验收未成立/⑱ 自指)=已闭合**。`display_probe()` 改读 `_draw_graph` 实际绘制记录、不再二次求值;第一轮判「不红」的 W1 注入(两处绘制条件 `→ if true`)本次在**呈现单元〔fv8〕与输入探针〔fv2〕两入口同时判红**,红原文与 M11 记录一致;⑱ 选区改按核心可见集/候选集/当前位独立选点、断言读绘制记录,自指解除;新变异(污染记录值)两入口亦红,证明记录值承重。 -- **D2(缺分支尖代码域全绿回执)=已闭合**。回执 `commit=de9cc102`=HEAD、`all_green=true`、`changed_vs_baseline=[]`,01/02/09 全绿,计数经自跑(1211 项 / 40 套件)独立印证。 - -## 4. 总判定:**通过(可合并)** - -甲案真关掉了 D1(W1 变异从「不红」变「双入口红」,实证接线层断言生效、记录非摆设、自指解除),D2 有分支尖全绿回执且与自跑读数相符。改动为只读观测面、零新画法/色值/线宽/缩放滚动/存档、无测试削弱、无静默越权(白名单外手段已备案并可整笔回退)。第一轮三条非阻塞观察项中,⑱ 自指(观察项 1)与 PASS 文案/计数(观察项 3)已随甲案修除;观察项 2(fv3 双 Boss 夹具手写 `type_vis`)按卡片 T2 指定写法保留、核心多 Boss 成员性由 `level_map_type_vis_test`〔fv3〕覆盖,维持非阻塞。 - -**唯一遗留(账面措辞,非阻塞、不构成缺漏)**:实现报告 §六 D2 行与 §5.7 行文字仍引用旧复跑 commit `8b9c6989`(甲案代码前的那次分支尖),而当前权威回执为 `de9cc102`;权威闸证据(`receipt.json`)本身正确、与 HEAD 一致、`all_green=true`,仅账面引用未随甲案后重跑刷新。合并前若再跑一次回执则会自然对齐;不影响 D2 闭合判定。 - -结论:卡④ D1/D2 两条缺漏均已闭合,无新削弱、无新越权,**通过,可合回 master**。 diff --git a/reports/iter34-反哺点-2026-10-22.md b/reports/iter34-反哺点-2026-10-22.md deleted file mode 100644 index c20caf38..00000000 --- a/reports/iter34-反哺点-2026-10-22.md +++ /dev/null @@ -1,104 +0,0 @@ -# 迭代 34 反哺点清单(2026-10-22) - -> 本批是代码实现批,`design/` 整目录与根规则文件只读。以下五条是实现完成后需要对设计文档与流程清单做的改写,逐条写清「哪个文件哪一节、改成什么、依据」。用户裁决后由文档批落文。 - -## ① 敌人系统.md:坐标记法与表校验措辞换代 - -- 文件与位置:`design/gdd/systems/战斗循环/敌人系统.md` :81(hobgoblin 缺口登记) -- 改成:坐标记法由「(1,3),即第一关第四节点」改为「(1, battle, 3)」。数据行位置未变(仍在第一关带 3),只换记法。 -- 依据:本卡契约 1(寻址键由 (关卡, 节点深度) 换成 (关卡, 行类, 深度带));冒烟 `smoke_all.gd` 的 hobgoblin 键位断言〔fv12〕按新记法钉死。 - -- 文件与位置:同文件 :278(表校验项清单中的行集构成违例一句) -- 改成:「行集内同敌种第二行 / 同坐标多行不相邻」→「同带键多行不相邻」;同时把「同坐标」类表述统一为「同带键」。 -- 依据:本卡契约 4(行集构成纪律由坐标相邻改为带键相邻);`enemy_table.gd` 的 `rowset_rule` 校验键串已换为 `"%d_%s_%d"`。 - -## ② 关卡节点地图.md:两处定位信息表述统一 - -- 文件与位置:`design/gdd/systems/冒险循环/关卡节点地图.md` :146 -- 改成:本句「本文递出关卡序号 + 节点编号,按编号生成单位归它」统一为「本文递出关卡序号+节点深度;敌人侧按深度带配池生成单位」。 -- 依据:同文件 :119 既有定稿句「关卡序号 + 节点深度……按深度带配池」,两处表述当前不一致;本批实现侧 `level_runner` 递出 `node_depth`、敌人侧 `band_of` 落带,以此为准。 - -## ③ 关卡节点图生成.md:产出规格补「多终点深度互异」条款 - -- 文件与位置:`design/gdd/systems/冒险循环/关卡节点图生成.md` :41–:48(产出约束五条) -- 改成:在产出约束中补一条——多终点(多 Boss 节点)必须落在互异的深度带;同一深度带出现两个终点即判产出无效(理由:共带会让「走哪条路打谁」落空)。 -- 依据:本卡契约 6 的图侧兑现处;实现侧 `config_db_cross_validator.gd` 已新增「同日图多 Boss 同带」报错(类别 `rowset_rule`,断言〔fv15〕),顶带消解另报 `level_node_dangling`〔fv16〕。 - -## ④ 待办清单.md:条目状态回写与退役记载 - -- 文件与位置:`待办清单.md` 中本卡登记条目 -- 改成:状态推进为「已实现(迭代 34)」并写明实现报告路径;按中间文档退役规则登记本卡与迭代产出物的退役去向。 -- 依据:五段流水线状态机(`doc-conventions` §12);实现批不写该文件,回写归批后收尾或用户发令。 - -## ⑤ roadmap.md:迭代 34 行批内状态刷新 - -- 文件与位置:`roadmap.md` 迭代 34 行(两卡并列,本卡为卡③)与顶部「当前位」节 -- 改成:卡③状态记为已实现;卡④(信息迷雾可见性)仍在同一迭代内推进;「当前位」按实际进度改写。 -- 依据:迭代实现批的收尾义务(`iter` 技能第 5 步账面随批);实现批不写该文件,换代归批后收尾或用户发令。 - -## ⑥ 关卡节点地图.md:调参表 N 状态行与可见性语义注记 - -- 文件与位置:`design/gdd/systems/冒险循环/关卡节点地图.md` :172(调参表「信息迷雾可见层数」行)、:100(可见范围「邻近 N 层」节) -- 改成:调参表状态由「定值批落数据表」改为「已落数据表 provisional 2(`level.tres` `info_visible_layers`)」;:100 节补实现语义三句——层距=入口 BFS 层差、Boss/终点与当前候选双豁免、单圈零记忆(已过节点回雾)。 -- 依据:卡④ T1 已实现并在 `level_runner.type_visible_ids()` 钉死;`level_map_type_vis_test` 五组断言〔fv1〕〔fv3〕〔fv4〕〔fv7〕覆盖。 - -## ⑦ 关卡节点图生成.md:屏预算约束行与「下一跳必在可见范围」兑现标注 - -- 文件与位置:`design/gdd/systems/冒险循环/关卡节点图生成.md` :105–114(调参表)、:83(「下一跳必在可见范围」) -- 改成:调参表补一行「图包络 ≤ 屏预算(1280×800 下约 10 层×8 槽;超预算只许限参数档,禁改画布冻结公式)」;:83 该句标注「实现侧已兑现:候选集强制豁免类型可见性」。 -- 依据:卡④契约 6 与 G6 定值(参数限图);`type_visible_ids()` 候选豁免断言〔fv4〕。 - -## ⑧ level-map.md:画布公式漂移订正+类型可见性层级行 - -- 文件与位置:`design/gdd/ux/level-map.md` :68(节点图区「冻结公式」句) -- 改成:宽 70+层深×122+34 → 70+层深×122+44.5(`NODE_HALF.x`36.5+8);高 52+槽位×58+26+8 → 52+槽位×58+25+24(`NODE_HALF.y`25+标签行 24);下限 620×240 → 620×256(抬高容纳标签行)。Information Hierarchy 节补「类型可见性(邻近 N 层显隐)」层级行。 -- 依据:`level_map_screen.gd:289–304` 现行 `_fit_canvas` 公式与大图拟合断言〔fv8〕;下限定值 256 是当前代码与渲染闸全绿的实际约束。**注意**:本条只订正文档记数,不动冻结件本身(画布公式语义与像素档位不变),是否接受改写由用户裁。 -- **呈报项(禁本批落文)**:「雾的正式视觉规格」(雾纹理/灰化遮罩/明度层)属冻结件域,本卡只做显隐语义最小呈现,视觉规格待用户裁后另立。 - -## ⑨ 术语表.md:登记「信息迷雾」并区别于「视野迷雾」 - -- 文件与位置:`design/gdd/术语表.md` -- 改成:新增词条「信息迷雾」=关卡节点图上按邻近层数隐藏节点类型的信息可见性;与「视野迷雾」(战斗棋盘按英雄曼哈顿半径遮挡)并列,注明两系统同词根易混、代码标识符禁 `fog_` 前缀。 -- 依据:卡④契约与风险 3;实现侧 API 全用 `info_visible_layers`/`type_visible_ids`/`node_display`/`display_probe`。 - -## ⑩ 关卡节点地图.md:Boss 描边口径冲突择一 - -- 文件与位置:`design/gdd/systems/冒险循环/关卡节点地图.md` :67 -- 改成:Boss 描边口径「封印黑 #000000」与 `level-map.md`/现役代码「紫赤+深墨环」不一致,请文档批核对后择一(现役实现与视觉终稿样张 v1 为紫赤+深墨环)。 -- 依据:卡④ G8 定值(实现随现役代码+level-map.md);本批未动描边视觉。 - -## ⑪ 待办清单.md:卡④条目状态按停卡实况回写 - -- 文件与位置:`待办清单.md` 中卡④登记条目 -- 改成:**不得记为「已实现」**——实现与代码域全量闸已完成(回执 `commit=6f547c35`、`all_green=true`),但独立验收审计判不通过、缺漏 D1(fv2 接线层验收与卡片 T2 对 `display_probe` 的定义互相矛盾)待用户裁决,卡④ 停在审计门。状态写「实现完成·审计不通过待裁(迭代 34)」,缺口记:「`display_probe` 只做决策面二次求值,`_draw_graph` 接线零断言;审计注入接线坏不红」;并写明实现报告路径。裁决后按用户选择推进(改验收接口 → 回炉 T2;改卡片 §7.1 口径 → 改卡后重交审计)。 -- 依据:`reports/iter34-卡④-验收审计-2026-10-22.md` §6 D1;五段流水线状态机(`doc-conventions` §12);实现批不写该文件,回写归批后收尾或用户发令。 - -## ⑫ roadmap.md:迭代 34 行状态刷新(卡③已完成、卡④停审计门) - -- 文件与位置:`roadmap.md` 迭代 34 行与顶部「当前位」节 -- 改成:卡③(敌人表深度带换代批)记为已实现并已合回;卡④(信息迷雾可见性批)记为「实现完成·独立验收审计不通过待裁」,迭代 34 不得标记完成;「当前位」按此实况改写。 -- 依据:迭代实现批收尾义务;实现批不写该文件,换代归批后收尾或用户发令。 - -## ⑬ 呈报项:超预算图策略升级路径 - -- 位置:无文档落点(用户在会话中裁) -- 内容:若后续定值批需要大于「约 10 层×8 槽」的完整可见图,两条路都动冻结件——等比缩放(动「20px 烘焙 1:1」「2/3/4px 折线」)或滚动视口(新交互范式)。本卡按 G6 走参数限图,两案均需用户裁。 -- 依据:卡④风险 2 与 G6 淘汰案。 - -## ⑭ test-conventions.md:验收断言应绑可观测提交面(方法论反哺) - -- 文件与位置:`design/engineering/test-conventions.md` §9.4(撤销/变异证明) -- 改成:补一句判据——端到端断言的判据应绑「被测改动特有的可观测提交面」(信号发射、事件、入参),不绑多层防御下的终态;否则单点变异可能被下游兜底吸收、终态不变而假绿。示例:探针「雾不扩可点域」首版只看屏/选路态终态,命中面放宽的变异仍绿;改绑 `route_chosen` 发射面后变异即红。 -- 依据:卡④ T4 实测(`提交=[3] visible=true 选路态=true`;终态与未点击一致)。 - -## ⑮ 体验验证清单.md:登记卡④三条实机目检项 - -- 文件与位置:`体验验证清单.md` -- 改成:登记三条待真人目检项(只登记、不阻塞)——①雾下可读性观感(未知类型节点是否一眼可辨为「未知」而非空节点);②多终点图 Boss 标记观感(双 Boss 是否都带描边且可辨);③大图完整可见观感(11 层×6 槽图是否完整落在屏内、编号不重叠)。判据与观测点见 `reports/iter34-实现报告-2026-10-22.md` §5.5。 -- 依据:卡④ T5「实机目检总清单三条」+iter 技能第 5 步账面随批;实现批禁写该清单,登记归文档批或用户发令。 - -## ⑯ 需求卡片:T2 的 `display_probe` 接口描述与 §7.1 fv2 口径一致化 - -- 文件与位置:`reports/需求卡片-信息迷雾可见性批-2026-10-21.md` :139(T2 第三条) -- 改成:「新增 `func display_probe() -> Dictionary`(id → `node_display` 快照,只读断言面,仿 `canvas_probe` 先例)」→「(id → 本帧实际绘制事实:图标/文字/描边/文案,由 `_draw_graph` 在实际绘制点写入,只读断言面)」,与 §7.1 fv2 真实入口格「真机 `display_probe()` 读绘制面(接线层验收)」对齐。 -- 依据:迭代 34 卡④ 独立验收审计 D1(`reports/iter34-卡④-验收审计-2026-10-22.md` §6);实现按 §7.1 验收面(甲案)落地,卡片 T2 的接口描述是滞后一侧,须由写卡段订正——实现期不改卡片。 diff --git a/reports/iter34-实现报告-2026-10-22.md b/reports/iter34-实现报告-2026-10-22.md deleted file mode 100644 index 36811584..00000000 --- a/reports/iter34-实现报告-2026-10-22.md +++ /dev/null @@ -1,278 +0,0 @@ -# 迭代 34 实现报告(2026-10-22) - -> 依据:`roadmap.md` 迭代 34 行与两份需求卡片。本批为代码实现批,产出物只写 `reports/`。 -> 卡③「敌人表深度带换代批」已完成;卡④「信息迷雾可见性批」另行施工(见第五节)。 - -## 一、批次概况 - -| 项 | 内容 | -|---|---| -| 工作树 | `D:\code\g-plan2-iter34-enemy`,分支 `iter34-enemy-band` | -| 开工基线(主区 master,603eba31) | 冒烟 1156 项检查 0 失败;单元 35 套件全绿;探针 `--click` 全绿 | -| 卡③收口读数 | 冒烟 1161 项检查 0 失败;单元 35 套件全绿;探针 `--click` 与 `--full` 全绿;静态扫描归零 | -| 合并前全量代码闸 | 见第四节 | - -## 二、卡③ 敌人表深度带换代批 - -### 2.1 任务卡状态 - -| T 卡 | 内容 | 状态 | -|---|---|---| -| T0 | 事实坐实与迁移表核对(§4.2 逐行) | 完成(迁移后 20 行键序与表序逐行一致〔fv11〕) | -| T1 | 带切法单点、resolver 三参签名、表键迁移、schema 校验换代 | 完成(提交 07f3eb55) | -| T2 | 跨表校验 ④ 敌侧带化、Boss 同带与顶带消解两条负向判据、消费面(叙述/结算)随动 | 完成(提交 07f3eb55) | -| T3 | 冒烟与单测断言面按 §8.1 S 清单换代、七项变异验证 | 完成(提交 61537da8、2 笔修补) | -| T4 | 三份产出物、独立验收审计、合并 | 进行中(本报告+审计+合并) | - -### 2.2 实际改动—验收对照表 - -生产侧关键落点:`game/data/schemas/enemy_table.gd`(`band_of` :157、`ROW_KINDS` :12、`row_kind` 枚举判据 :57–:58、锚行与 Boss 行集 :149);`game/core/config_db_resolver.gd`(三参签名 :33、带寻址与两档告警 :44–:80);`game/core/config_db.gd`(门面 :193);`game/core/config_db_cross_validator.gd`(④ 敌侧带化 :94–:106);`game/core/unit_container.gd`(行类参数 :38、解析 :43);`game/core/battle_orchestrator.gd`(行类取值 :328、透传 :359);`game/presentation/battle_narrator.gd:76`;`game/data/tables/enemy.tres`(20 行键迁移)。 - -| fv | 需求点(简) | 验收断言(文件:行) | 层级 | 真实入口 | 撤销/变异结果 | -|---|---|---|---|---|---| -| fv1 | 带命中偏难一侧 | `config_db_resolver_test.gd:58`、:61 | 单元 | `resolve_enemy_rowset` 直调 | 变异 `band_of` 改 floor → 红(实测两断言均红) | -| fv2 | 行集按表文件行序返回 | `config_db_resolver_test.gd:67`、:71 | 单元 | 同上 | 变异混编两行对调 → 红 | -| fv3 | 越顶带取顶带+告警去重 | `config_db_resolver_test.gd:78`、:84、:87 | 单元 | 同上(D 高于顶带、重复请求两次) | 变异 floor → 红(实测) | -| fv4 | row_kind 路由 | `config_db_resolver_test.gd:93` | 单元 | 同 D 分别传 battle/boss | 撤销第三参 → 双 kind 不再分域 | -| fv5 | 越域与非法行类致命 | `config_db_resolver_test.gd:101`、:104、:107 | 单元 | `resolve_enemy_rowset(0,…)`/`(L,-1,…)`/`(1,0,"elite")` | 撤销判据 → 不报致命 | -| fv6 | 生产表加载校验零错误 | `config_db_test.gd:14` | 接口 | `ConfigDB.load_tables` | 回退键迁移 → 校验红 | -| fv7 | 非法 row_kind 被拒 | `config_db_test.gd:256` | 接口 | `EnemyTable.validate` 非法枚举夹具 | 变异注释枚举判据 → 红(实测) | -| fv8 | dup_key 新键拦截 | `config_db_test.gd:235` | 接口 | 同带键同敌种二行夹具 | 回退键串 → 漏拦 | -| fv9/fv20 | 同带隔断报 rowset_rule | `config_db_test.gd:225` | 接口 | 隔断夹具 | 回退 rowset_rule 键串 → 漏拦 | -| fv10 | 锚行与 Boss 行集缺失报 anchor | `config_db_test.gd:265`、:273 | 接口 | 缺锚/缺 Boss 夹具 | 回退 anchor 判据 → 漏报 | -| fv11 | 20 行计数与键序 | `smoke_all.gd:2106` | 接口 | 加载全表逐行比对 | 变异对调/迁错带 → 红(实测) | -| fv12/fv25 | hobgoblin 在 (1,battle,3) 留任 | `smoke_all.gd:2114`、:2116 | 接口 | 同上读表面+表注释件核 | 变异迁错带 → 红(实测) | -| fv13 | 三 Boss 行类与带键就位 | `smoke_all.gd:2132` | 接口 | 同上读表面 | 回退任一行类 → 红 | -| fv14 | 值列零变更 | git diff 件核 | 件核 | `git diff` 逐值列比对 | 改任一值列 → 件核红 | -| fv15 | 同关双终点共带报错 | `config_db_cross_validator_test.gd:137` | 单元 | `cross_validate()` 双 Boss 同带夹具 | 变异删判据 → 红(实测) | -| fv16 | 顶带消解报错 | `config_db_cross_validator_test.gd:142` | 单元 | 节点深度高于顶带夹具 | 变异判据短路 `if false` → 红(实测) | -| fv17 | 单终点图零误报 | `config_db_cross_validator_test.gd:132` | 单元 | level 1 合成单关夹具(`display_name:"夹具关"`);现役三关零误报由 fv6 兜住 | 回退 ④ 敌侧换代 → 红 | -| fv18 | 混编行序 | `smoke_all.gd:4815` | 集成 | 冒烟读 (2,battle,5) 行集 | 变异对调两行 → 红(实测) | -| fv19 | 混编骰和 ≤ 4 | `smoke_all.gd:4820` | 集成 | 同行集静态求和 | 改运行期守卫 `ROWSET_INSTANCE_MAX` → 同段守卫断言(「两行各定值 3(总 6>4)spawn 被拒」)红;静态求和本身放宽不会红 | -| fv21 | 首遇带数据驱动定位 | `smoke_all.gd:2282` | 集成 | 扫表取最低带 | 撤销扫表定位 → 取键失配 | -| fv22 | 巨蜘蛛首遇带固定 2 不加骰 | `smoke_all.gd:4801` | 集成 | 扫表定位+行集读数 | 变异加 `unit_count_max: 3` → 红(实测) | -| fv23 | bugbear/duergar 首遇带固定 1 | `smoke_all.gd:3473`、:4859 | 集成 | 扫表定位+行集读数 | 变异加 `unit_count_max` → 红 | -| fv24 | V7 S 清单 17 行逐条换代(尾缀=清单件核;随带定点断言 = Boss 行集与毒吐投放面 + S15 叙述寻址) | `smoke_all.gd:4281`、:4291、:4807、:4865;`battle_narrator_test.gd:81`、:82 | 集成+单元 | 扫表定位+Boss 行集;`boss_name(runner, db)` 经 `resolve_enemy_rowset(..., "boss")` | 变异 `boss_name` 行类 "boss"→"battle" → `battle_narrator_test` fv24 红(实测);逐条勾销见 §2.2.1 | -| fv25 | hobgoblin 缺口登记随行 | `smoke_all.gd:2116` | 接口 | 表文件注释在位 | 同 fv12 | -| fv26 | V9 反哺点五落点(非行为改动) | `reports/iter34-反哺点-2026-10-22.md` ①–⑤ | 件核 | 反哺点文件与卡片 §6 同步面表逐条比对 | 删任一落点 → 件核红(文档产出物,无代码断言) | - -覆盖说明:`game/` 全域共 37 行带 fv 尾缀(36 条断言,另 `smoke_all.gd:969` 一行是注释),覆盖 fv1–fv26;fv14(值列零变更)与 fv26(反哺点落点)按卡片约定走件核,代码侧无断言;fv9 与 fv20、fv12 与 fv25 各自共用同一断言行,计数时不重复。 - -#### 2.2.1 V7:S 清单 17 行逐条勾销(卡片位置 → 现工作树位置) - -| S | 现工作树位置 | 换代内容 | 勾销凭据 | -|---|---|---|---| -| S1 | `smoke_all.gd:3168`/:3491/:4499 | 三参化(row_kind="battle")+带语义注释 | 三处均显式传 "battle",行集首行读数随带键 | -| S2 | `smoke_all.gd:4795`(断言 :4801〔fv22〕) | 首遇带数据驱动定位+恰一行/unit_count=2/无 unit_count_max | 〔fv22〕实测绿;变异加 unit_count_max → 红(§2.3 第 5 项) | -| S3 | `smoke_all.gd:4802`(断言 :4807〔fv24〕) | L2 Boss 行集改 `_boss_depth(2)`+"boss" | 〔fv24〕实测绿 | -| S4 | `smoke_all.gd:4810`(断言 :4815〔fv18〕、:4820〔fv19〕) | 键改 (2,battle,5),行序契约保持 | 〔fv18〕变异对调 → 红(§2.3 第 6 项) | -| S5 | `smoke_all.gd:4852`/:4860(断言 :4859〔fv23〕、:4865〔fv24〕) | 首遇带定位+Boss 三参化 | 两条断言实测绿 | -| S6 | `smoke_all.gd:974-984`/:4781-4789/:5256-5258 | en_keys 改 (L,row_kind,depth_band),Boss 节点查 boss 键域 | 三处键串 `"%d_%s_%d"`;L2/L3 跨表行完整断言绿 | -| S7 | `smoke_all.gd:2263`/:2279/:2301 | Boss AC 台阶保留;(1,2) 锚定改 bugbear 首遇带定位〔fv21〕;曲线改按 (1,battle) 带键相邻比 ≤ 2.5 | 三条断言实测绿;fv21 变异见 §2.3 第 8 项 | -| S8 | `smoke_all.gd:4262-4306` | 毒吐投放面改 `depth_band > 巨蜘蛛首遇带`+池键含 row_kind | 〔fv24〕:4281/:4291 实测绿;键串 `2_battle_5_giant_spider` 等 | -| S9 | `smoke_all.gd:3460-3474`(断言 :3473〔fv23〕) | (1,2) 改 bugbear 首遇带定位(固定 1 不加骰) | 〔fv23〕实测绿;数据侧变异下 fv23 红(§2.3 第 8 项) | -| S10a | `smoke_all.gd:2024`/:2674/:2699/:3078/:3143/:3230/:3284/:3324/:3354/:3566/:3633/:3651/:3668/:4139/:4359/:4606/:4970/:5040;`damage_probe.gd:95` | 各补 `"node_type": "boss"` | 上述直呼点均带 node_type;冒烟 1161 项 0 失败 | -| S10b | `smoke_all.gd:2674`/:2699 | 改 (2,9)+boss | 两处均为 `"node_depth": 9, "node_type": "boss"` | -| S11 | `tests/unit/core/config_db_resolver_test.gd`(fv1–fv5) | 换代为带寻址语义断言 | fv1–fv5 实测绿;floor 变异红(§2.3 第 1 项) | -| S12 | `tests/unit/data_config/config_db_test.gd`(fv6–fv10) | 夹具键改 row_kind/depth_band,签名三参化 | fv6–fv10 实测绿;枚举判据变异红(§2.3 第 2 项) | -| S13 | `tests/unit/core/config_db_cross_validator_test.gd`(fv15–fv17) | 夹具键改名+补两条负向用例 | fv15/fv16 变异红(§2.3 第 3、4 项) | -| S14 | `smoke/skill_rhythm_probe.gd:17`;`smoke/damage_probe.gd:269`/:422 | 三参化 | 三处显式传 row_kind | -| S15 | `game/presentation/battle_narrator.gd:76`;验收 `tests/unit/presentation/battle_narrator_test.gd:81`/:82〔fv24〕 | 三参化(row_kind="boss")+补真实入口单测 | 实测绿;变异 "boss"→"battle" → fv24 红(§2.3 第 9 项) | -| S16 | `smoke_all.gd:3491`(断言 :3496) | erow2 首行读数随带键(三参化) | 全深度×12 种子 60 场断言实测绿 | - -勾销口径:S11–S13 归单测面,S1–S10、S14–S16 归冒烟/探针面,S15 归消费面;17 行全部给出工作树位置与凭据,无一行停在旧坐标语义。 - -### 2.3 变异验证记录表(注入 → 最窄闸见红 → 还原) - -| # | 注入内容 | 最窄闸 | 红断言(原文摘要) | 还原确认 | -|---|---|---|---|---| -| 1 | `EnemyTable.band_of` 的 `>=` 改为 `<=`(反向 floor) | `config_db_resolver_test.gd` | 带命中取偏难一侧:请求深度 4 命中带 5〔fv1〕;精确落带〔fv1〕;越顶带 clamp〔fv3〕×2 | 改回 `>=`,复跑全绿 | -| 2 | 注释掉 `validate` 的 `row_kind ∈ ROW_KINDS` 判据 | `config_db_test.gd` | 非法 row_kind 被拒(封闭枚举)〔fv7〕 | 恢复判据,复跑全绿 | -| 3 | 删除跨表校验「Boss 同带」判据 | `config_db_cross_validator_test.gd` | 同关双终点共带报 rowset_rule〔fv15〕 | 恢复判据,复跑全绿 | -| 4 | 顶带消解判据短路为 `if false` | `config_db_cross_validator_test.gd` | Boss 节点越顶带报 level_node_dangling〔fv16〕 | 恢复判据,复跑全绿 | -| 5 | 巨蜘蛛首遇行(2,battle,4)加 `unit_count_max: 3` | 冒烟全量 | 巨蜘蛛首遇带行集 = {giant_spider} 恰一行且 unit_count=2 无 unit_count_max〔fv22〕 | 删该字段,冒烟复跑全绿 | -| 6 | 混编两行(2,battle,5)文件内对调 | 冒烟全量 | 混编行序 [bugbear→giant_spider]〔fv18〕;20 行键序〔fv11〕;行值复制面 | 还原行序,冒烟复跑全绿 | -| 7 | hobgoblin 迁错带键(迁到 (1,battle,2)) | 冒烟全量 | 首次注入在加载期即被跨表校验拦下:`ERROR: [ConfigDB/level_node_dangling] level 1 battle 节点深度 3 高于 (level 1, battle) 顶带键 2——顶 clamp 消解`,随后〔fv6〕红(config_db_test 2/2 项止步);「同带相邻」判据未触发——该行紧接 bugbear 带 2 行之后、文件内相邻。二次注入改为与 band 2 行互换带键(表可加载),命中「hobgoblin 行在 (1,battle,3)」〔fv12〕〔fv25〕与 20 行键序〔fv11〕 | 还原带键,冒烟复跑全绿 | -| 8 | 数据侧:L1 bugbear 行 `depth_band` 2→1(V6① 撤销证明换代) | 冒烟全量 | 单跑数据变异时〔fv21〕仍绿(首遇带随之变 1、锚定行随迁——数据驱动的正证),连带〔fv11〕〔fv23〕与 spawn 扫描等共 6 项红;叠加「`_first_encounter_band` 写死返回 2」后〔fv21〕红(期望带 2 找不到 bugbear 行),共 9 项红 | 两处均 `git checkout --` 还原,冒烟 1161 项 0 失败 | -| 9 | `battle_narrator.boss_name` 行类 "boss"→"battle"(S15 撤销证明) | `battle_narrator_test.gd` | 「Boss 名取自 Boss 行集首行(期望「食人魔酋长」实际「大地精」)〔fv24〕」红,退出码 1 | 改回 "boss",13 项检查全绿 | - -拦截边界如实记录:第 7 项按卡片写法直接把 hobgoblin 放到带 2 时,首条红是跨表 `level_node_dangling`(战斗节点深度 3 落到不存在的顶带),不是同带相邻规则——该行紧接 bugbear 带 2 行之后、文件内相邻,隔断判据不触发;键位断言的敏感度改用「与带 2 行互换带键」的表内合法变异单独观测。第 8 项单跑数据变异时〔fv21〕仍绿,说明该断言跟的是数据而不是写死值;只有「数据已变、坐标仍写死」的组合才让〔fv21〕红,这是它能区分两种实现的边界。另:第 1 项连带 fv3 变红,属同一算法单点的预期耦合,不另计。 - -### 2.4 卡片与实际改动面的偏差(逐条说明) - -1. **S10a 补位**:卡片 S10a 列了 11 个补 `node_type: "boss"` 的位置,实际还需在 `smoke_all.gd` 的 CLEAR_SMOKE_DEPTH 段与 `(2,9)` 段若干直呼点补同一字段,并把 `spawn(1,4)`、深度循环 `spawn` 补行类参数。理由:这些点同样按 Boss 语义取行集,漏补会让它们落回 battle 键域、读到错的敌人行。语义未变,只是行类改为显式传。 -2. **gp 掉落断言改数据驱动**:原断言把「破财压制」战斗的 `gp_dropped` 与普通档掷定区间硬编码比对;该战斗在 S10a 中被标为 Boss 语义后,掉落档合法切换到 Boss 档,硬编码区间不再成立。改为从 `shop.tres` 读 Boss 档区间与关级加值再比对(`shop_gp_drop_row`/`shop_level_gp_bonus`),断言强度不变且不再写死数值。 -3. **单测补行类参数**:`skill_unlock_test.gd` 的 Boss 用例原先直呼 `spawn(2,9)` 取默认 battle 行类,会落错带;改为显式传 `"boss"`,普通行零扰动用例保持 `"battle"`。 -4. **静态扫描两处归零**:新 resolver 的行集容器改为显式类型(原为无类型 `Array`);表注释里的退役词改为白话。两处都不改游戏行为。 - -### 2.5 实机目检总清单(只登记,不阻塞合并) - -| # | 项目 | 观测点 | 状态 | -|---|---|---|---| -| 1 | 三关实机开战遭遇观感不回归 | 第一关地精/熊地精、第二关巨蜘蛛与重击手混编、三关 Boss 战的敌人数量与外观与改动前一致 | 待用户试玩 | - -### 2.6 遗留读数义务 - -- 数值零变更,本卡不发起模拟跑批(MC 档位规则)。 -- 合并后主区需 `--editor --quit` 重刷类名缓存,再跑一次全量代码闸复跑,读数写入合并提交信息。 - -## 三、卡③ 独立验收审计 - -审计报告:`reports/iter34-验收审计-2026-10-22.md`(由未参与实现的子代理执行;只读卡片、本报告、diff、测试与闸回执,自跑最窄命令复现,不采信「已全绿」)。 - -**第一轮结论:不通过**——缺漏 4 条+描述不符 4 条(逐行依据见该报告第一至四节)。处置如下: - -| 缺漏 | 处置 | 落点 | -|---|---|---| -| V7 无覆盖行、fv24 尾缀挂错需求点 | 补 V7 覆盖行+S 清单 17 行逐条勾销表 | 本报告 §2.2 fv24 行、§2.2.1 | -| V9/fv26 覆盖行缺失 | 补件核行(反哺点文件 ①–⑤) | 本报告 §2.2 fv26 行 | -| S15 `boss_name` 换代无断言 | 补真实入口单测(扫表取 Boss 带键 → 断言叙述名=boss 行集首行名),并做撤销证明 | `game/tests/unit/presentation/battle_narrator_test.gd:56-82`〔fv24〕;§2.3 第 9 项 | -| V6①/fv21 撤销证明按记录无法变红 | 改为数据侧变异:bugbear 带 2→1(fv21 仍绿=数据驱动正证)+叠加写死坐标(fv21 红) | §2.3 第 8 项 | - -描述不符 4 条已按审计建议改写:fv17 夹具措辞(level 1 合成单关,非现役三关,现役三关由 fv6 兜住)、fv19 撤销归属(运行期守卫断言,静态求和本身不红)、§2.3 第 7 项拦截首红(跨表 `level_node_dangling`)、断言计数(36 条断言+1 行注释)。 - -**第二轮复审结论:通过**(`reports/iter34-验收审计-复审-2026-10-22.md`,被审 HEAD `68009b4e`)——A–F 六项全通过:V7 覆盖行与 17 行勾销表齐备(抽 6 行实读属实)、fv26 件核行在位、S15 单测走真实入口且变异红、V6① 数据侧撤销证明复现一致、四条描述不符已按实改写、修补未引入未申报的测试削弱。卡③ 合并前置满足。 - -## 四、全量代码闸读数 - -`tools/run_all_gates.sh --code`,起跑 commit `8b18e057`(2026-10-22 04:42),回执 `game/tests/.gates/receipt.json`: - -| 闸 | 退出码 | 指纹 | 对基线 | -|---|---|---|---| -| 01 冒烟全量 | 0 | green | 一致 | -| 02 单元套件 | 0 | green | 一致(35 套件) | -| 03 探针 --click | 0 | green | 一致 | -| 04 探针 --full | 0 | green | 一致 | -| 05 渲染闸 | 0 | green | 一致(shot_probe 七屏+golden 逐像素) | -| 09 静态扫描 | 0 | green | 改善(基线红 → 现绿,本卡改动面归零) | -| 10 熵增闸 | 0 | green | 一致 | -| 11 回放确定性对拍 | NEED-APPROVAL | skip | 本卡数值零变更,按档位规则不发起(需用户批准) | - -`all_green: true`,`changed_vs_baseline: []`。改前基线(T0,主区 master `603eba31`):冒烟 1156 项检查 0 失败/单元 35 套件 0 失败/探针全绿。 - -> 环境注记:本轮曾出现一次阶段性假红(02/03/04/05 同时红)——前一次被中断的闸进程未死透,与新闸并行写了同一批 `game/tests/.gates/*.log`(日志内出现同一探针同时 PASS 与 FAIL 的交错字节),另有并行会话在同机跑另一 worktree 的闸。清场后端到端重跑得本次读数;单件直跑(`ui_struct_probe` 等)在清场前即为绿,佐证为环境并发而非行为回归。 - -## 五、卡④ 信息迷雾可见性批 - -### 5.1 工作树与任务卡状态 - -工作树 `D:\code\g-plan2-iter34-fog`,分支 `iter34-fog`,起自卡③合并后的 master `43c0f3c1`。提交:`ed8e0f9f`(T1–T3 实现与单测/冒烟/探针)、`4ef154b9`(⑱ 零提交判据改绑信号面)、`6f04f29b`(类型闸归零+探针补雾下文字通道断言)、`1970e0ef`(审计 D1 回炉 T2:绘制记录观测面+⑱ 选区解耦)。 - -| T 卡 | 内容 | 状态 | -|---|---|---| -| T0 | 事实坐实与基线读数 | 完成(改前基线:冒烟 1206 项 0 失败/单元套件全绿/探针全绿) | -| T1 | N 落表+越域校验+`type_visible_ids()`+核心套件 | 完成(提交 `ed8e0f9f`) | -| T2 | `node_display` 决策面+`display_probe`+`_draw_graph` 接线+呈现套件 | 完成(提交 `ed8e0f9f`,类型闸修补 `6f04f29b`) | -| T3 | 探针常数锚同源化+大图夹具(11 层×6 槽) | 完成(提交 `ed8e0f9f`) | -| T4 | 冒烟断言+探针⑱+变异汇总+代码域全量闸 | 完成(见 §5.4、§5.7) | -| T5 | 三份产出物+独立验收审计+合并 | 审计两轮完成(首轮不通过→甲案回炉→复审通过),合并进行中 | - -### 5.2 实际改动—验收对照表 - -生产侧落点:`game/core/level_runner.gd`(`type_visible_ids()` :422–440、`_bfs_layers()` :443–462);`game/presentation/views/level_map_screen.gd`(`node_display()` :420–437、`_draw_graph` 消费 :478、`display_probe()` :536–547);`game/data/schemas/level_table.gd:8`(字段)、:23–24(越域校验);`game/data/tables/level.tres:7`(`info_visible_layers = 2`);`game/smoke/ui_input_probe.gd:142–145`(常数锚同源化)、:44–90(case⑱);`game/smoke/smoke_all.gd:4938–4943`(真图可见性断言组)。 - -| fv | 需求点(简) | 验收断言(文件:行) | 层级 | 真实入口 | 撤销/变异结果 | -|---|---|---|---|---|---| -| fv1 | 邻近 N 层见类型 | `level_map_type_vis_test.gd:121`、:122、:123(层距边界);:142、:143(入口锚层 0);:166、:167、:168(单链退化);`smoke_all.gd:4942`(真图) | 单元+集成 | `LevelRunner.type_visible_ids()` 直调;冒烟真图读面 | M1 `<=N`→`= 10`),规模不变、语义不变。 -2. **⑱ 零提交判据改绑信号面**:按卡片原写法(命中面放宽到全节点 → ⑱ 红)首轮实测仍绿——点击确实穿透并提交,但雾下非战斗节点让本屏保持激活、运行器候选校验又把非法编号拒掉,终态与未点击时相同。改绑 `route_chosen` 发射面后单点变异即红(`提交=[3]`)。属卡片 §8 L1「断言写法」自救范围;同时给⑱ 各断言补 fv2/fv3/fv4/fv9 尾缀。 -3. **类型闸新增红**:T4 首轮全量闸暴露新增 4 处 R1(`Dictionary` 未参数化 1 处、`Array` 未参数化 3 处),已参数化归零(`6f04f29b`);修复后 09 转绿(相对基线为改善)。 -4. **常数锚行号**:卡片写 `ui_input_probe.gd:98-99`,工作树实落点 :142–145(T0 复核行号),同源化方式=直接引 `LevelMapScreen` 导出常量,未新增副本。 -5. **画布下限漂移**:`_fit_canvas` 现行为 `max(Vector2(620, 256))`,`level-map.md:68` 冻结公式写「下限 620×240」——本卡不动冻结件,登记反哺点。 -6. **命中面零改动**:`_candidate_at`(:373–386)卡内要求零改动,实现保持候选集循环,fv9 的变异证明正落在此函数。 - -### 5.4 变异验证记录表(注入 → 最窄闸见红 → 还原) - -| # | 注入内容 | 最窄闸 | 红断言(原文摘要) | 还原确认 | -|---|---|---|---|---| -| M1 | `type_visible_ids` 层距判定 `<= n` 改 `< n` | `level_map_type_vis_test` | 层距 N 内可见:锚层 1 取节点 4(层距 2)〔fv1〕;单链退化形态:入口锚层 0,下标 2 可见〔fv1〕(连带 fv6/fv7 各 1 条) | `git checkout` 还原,26 项全绿 | -| M2 | `node_display` 的 `draw_icon` 强制 true | `level_map_info_fog_test` | 雾下节点:图标与类型文字均不绘制〔fv2〕;Boss 豁免不扩散〔fv3〕;大图远处节点入雾〔fv8〕 | 还原,19 项全绿 | -| M3 | 删 `type_visible_ids` 的 Boss 豁免分支 | `level_map_type_vis_test` | Boss 恒显:层距 6 的末位 Boss 仍在可见集〔fv3〕;单链编号空间=数组下标〔fv1〕 | 还原,26 项全绿 | -| M4 | 删 `node_display` 的 Boss 豁免(`type_shown` 只看可见集) | `level_map_info_fog_test` | Boss 豁免独立于可见集:不在 type_vis 的 Boss 仍出示类型〔fv3〕 | 还原,19 项全绿 | -| M5 | 删 `type_visible_ids` 候选豁免追加段 | `level_map_type_vis_test` | 候选恒显:跨层跳边候选(层距 N+1)仍在可见集〔fv4〕 | 还原,26 项全绿 | -| M6 | `label_text` 拼接 `node.get("enemy_id")` | `level_map_info_fog_test` | 决策面不消费内容键〔fv5〕;label_text 不含内容值(实际「战斗goblin_secret · 可选」)〔fv5〕 | 还原,19 项全绿 | -| M7a | 删 `LevelTable.validate` 越域判据(`if false`) | `level_map_type_vis_test` | N 越域 0/-1:validate 判 domain 错〔fv6〕 | 还原,26 项全绿 | -| M7b | `type_visible_ids` 的 N 硬编码 2(不读表) | `level_map_type_vis_test` | N 读表:N=1 时节点 4 出窗〔fv6〕;N=3 时节点 5 入窗〔fv6〕;N≥层高全可见〔fv6〕 | 还原,26 项全绿 | -| M8 | 可见集加 `visited_ids().has(nid)`(累积可见) | `level_map_type_vis_test` | 单圈回雾:已过远端节点 0(层距 5)不因 visited 豁免〔fv7〕;中距节点 2 不可见〔fv7〕 | 还原,26 项全绿 | -| M9 | `_fit_canvas` 宽公式去 `NODE_HALF.x` 项 | `level_map_info_fog_test` | 大图拟合:画布 min.x 覆盖最深节点(1298.0 ≥ 1334.5)〔fv8〕;全节点落在画布内〔fv8〕 | 还原,19 项全绿 | -| M10 | `_candidate_at` 命中面放宽到全节点 | `ui_input_probe`(⑱) | 雾下节点点击产生提交(提交=[3] visible=true 选路态=true)〔fv9〕 | `git checkout` 还原,探针 PASS | -| M11 | `_draw_graph` 图标/文字两处条件改 `if true`(绕开决策面=接线坏) | `level_map_info_fog_test`+`ui_input_probe`(⑱) | 大图呈现:入口停点远处战斗节点(编号 32,层 6)类型入雾〔fv8〕;case⑱ 雾下节点类型仍出示(图标=true 文字=true——范围外只见结构)〔fv2〕 | `git checkout` 还原,19 项全绿+探针 PASS | - -### 5.5 实机目检总清单(只登记,不阻塞合并) - -| # | 项目 | 观测点 | 状态 | -|---|---|---|---| -| 1 | 雾下可读性观感 | 未知类型房间是否一眼可辨为「未知」而非「漏画」;编号仍在、类型图标与文字不出现 | 待用户试玩 | -| 2 | 多终点 Boss 标记观感 | 双 Boss 图上两个终点的紫赤描边是否都清楚可辨 | 待用户试玩 | -| 3 | 大图完整可见观感 | 大图(11 层×6 槽)整幅在画布内、无节点被裁切或挤在一角 | 待用户试玩 | - -### 5.6 遗留读数义务 - -- 数值零变更(`info_visible_layers` 是呈现参数,不进战斗结算),本卡不发起模拟跑批(MC 档位规则)。 -- 合并后主区需 `--editor --quit` 重刷类名缓存,再跑一次代码域全量闸复跑,读数写入合并提交信息。 -- 合并前回执须与分支尖同 commit(回执 `commit` 字段)。 - -### 5.7 全量代码闸读数 - -T4 里程碑跑批 `tools/run_all_gates.sh --code`,起跑 commit `4ef154b9`: - -| 闸 | 退出码 | 对基线 | -|---|---|---| -| 01 冒烟全量 | 0 | 一致(1211 项检查 0 失败) | -| 02 单元套件 | 0 | 一致(40 套件,含本卡新增 2 套件) | -| 03 探针 --click | 0 | 一致 | -| 04 探针 --full | 0 | 一致 | -| 05 渲染闸 | 0 | 一致 | -| 09 静态扫描 | 1 → 0 | 新增 4 处 R1(容器未参数化)→ `6f04f29b` 修复后归零,由红转绿(改善) | -| 10 熵增闸 | 0 | 一致 | -| 11 回放确定性对拍 | NEED-APPROVAL | 本卡数值零变更,按档位规则不发起(需用户批准) | - -`6f04f29b` 修复后窄验证:`audit_type_hints` PASS;两套件 26+19 项全绿;`ui_input_probe` PASS。 - -D1 回炉后在分支尖复跑(`de9cc102`,含代码笔 `1970e0ef`):`all_green: true`、`changed_vs_baseline: []`、01 冒烟 1211 项 0 失败、02 单元 40 套件全绿、03/04 探针绿、05 渲染闸绿、09 静态扫描 0(对基线「改善」)、10 熵增闸 0、11 对拍按档位规则 NEED-APPROVAL。此前那一次复跑(`8b9c6989`,甲案代码之前)已被本次取代。 - -## 六、卡④ 独立验收审计 - -审计报告:`reports/iter34-卡④-验收审计-2026-10-22.md`(未参与实现的子代理执行;被审 HEAD `6757c21e`;自跑 40 套件、探针、冒烟 1211 项,另自设变异注入)。 - -**第一轮结论:不通过**——2 条缺漏,无测试削弱、无契约破坏、无越权自决、无数值/RNG 违规。M1/M2/M9/M10 四条变异由审计自跑,「红的位置与报告 §5.4 逐字一致,无复现不一致」。 - -| 缺漏 | 内容 | 本批处置 | -|---|---|---| -| D2 回炉 T4/T5 | 缺与分支尖同 commit 的 `--code` 全绿回执(旧回执停在 `4ef154b9`、09 红、`all_green=false`) | **已闭合**:在分支尖重跑全量代码域闸,回执 `commit=de9cc102`(甲案代码回炉之后重跑)、`all_green=true`、`changed_vs_baseline=[]`(见 §5.7) | -| D1 回炉 T2 | fv2「接线层验收」未成立:`display_probe()` 是逐节点重调 `node_display` 的快照,与 `_draw_graph` 的实际绘制是两次独立求值;审计注入「`_draw_graph` 两处条件改 `if true`」模拟接线坏,单元 19 项与探针仍全绿。审计判为**卡片 T2 对 `display_probe` 的定义与 §7.1 fv2「读绘制面」要求互相矛盾(spec-AC 不一致)** | **甲案回炉(提交 `1970e0ef`)**:呈报后用户指令「继续未完成的任务」,按呈报推荐案甲落地——`_draw_graph` 在描边/图标/文字三处实际绘制点写入本帧绘制事实(新只读成员 `_drawn_disp`),`display_probe()` 改读该记录、不再二次求值;探针⑱ 选区改按核心 `type_visible_ids()` 独立选雾节点,解除「按记录选、再断言记录」自指;单元大图组两处呈现断言随动读绘制记录。新增撤销证明 M11(绕开决策面直接画 → 单元〔fv8〕与探针〔fv2〕双红)。该项在 §9 白名单外,已在代拍板追认单第 7 行备案,可整笔回退。 | - -审计附三条非阻塞观察项,处置:第 1、3 条已随甲案修除,第 2 条按卡片 T2 指定写法保留(核心多 Boss 成员性由 `level_map_type_vis_test` 的〔fv3〕用例覆盖同一分支)—— - -1. 探针 ⑱ 的「雾下图标不出示」判据自指——`fog_id` 正是按 `not draw_icon` 选出再断言其 `not draw_icon`,该半句恒真;有效的是文字通道半句与存在性判定(M2 时因选不到雾节点走「选取失败」分支变红)。建议选区与断言解耦(按层距/候选集独立选雾节点)。 -2. fv3 双 Boss 夹具手写 `type_vis=[1,7,9]`,未从 `type_visible_ids()` 推导两端点均在集;核心多 Boss 成员性由单 Boss 用例覆盖同一分支,风险低。 -3. 探针 PASS 文案只列到 ⑰、未点名 ⑱,§5.2 原称「探针⑱ 5 项断言」实为 4 个 `_pre_fail` 判据+1 个选区前置。两处均已订正:⑱ 已列入 PASS 文案,计数改写为「4 判据+1 前置」。 - -### 6.1 第二轮复审结论(`reports/iter34-卡④-验收审计-复审-2026-10-22.md`) - -被审 HEAD `de9cc102`(=分支尖=回执 commit)。执行者仍是未参与实现的子代理,自跑呈现单元、核心单元、输入探针与冒烟读数,并自己复现第一轮判「不红」的那发注入。 - -| 项 | 结论 | 复审自跑证据 | -|---|---|---| -| 项 1 D1 是否真关 | **通过** | 注入 `_draw_graph` 图标/文字两处条件改 `if true` → 呈现单元〔fv8〕与探针〔fv2〕双红,探针红原文 `case⑱ 雾下节点类型仍出示(图标=true 文字=true——范围外只见结构)〔fv2〕`;另发「只污染记录值」的新变异同样双红,证记录承重 | -| 项 2 ⑱ 自指解除 | **通过** | 选区改按核心 `type_visible_ids()`/候选集/当前位独立选点,断言读绘制记录,两侧不同面 | -| 项 3 新削弱/新越权 | **通过** | 无删断言、无放宽阈值(探针常数锚 26→36.5 属变严,容差 -0.5 未动);新增面为只读观测,零新画法/色值/线宽/缩放滚动/存档;白名单外手段已在代拍板第 7 行备案、可一笔回退 | -| 项 4 D2 与账面相符 | **通过** | `receipt.json` `commit=de9cc102`、`all_green=true`、`changed_vs_baseline=[]`;自跑冒烟 1211 项 0 失败、单元 40 套件绿 | - -**闭合判定:D1 已闭合、D2 已闭合,总判定通过(可合并)。** 观察项 1 与 3 已随甲案修除;观察项 2 按卡片 T2 指定写法保留,核心多 Boss 成员性由 `level_map_type_vis_test`〔fv3〕覆盖,维持非阻塞。复审指出的一处账面措辞(§5.7 与 §六 仍引旧复跑 commit `8b9c6989`)已在本节随文订正。 - -**合并状态:两轮审计均通过,合回 master。** 卡③ 已在 `43c0f3c1` 合回。 - diff --git a/reports/iter34-验收审计-2026-10-22.md b/reports/iter34-验收审计-2026-10-22.md deleted file mode 100644 index 04f38ea5..00000000 --- a/reports/iter34-验收审计-2026-10-22.md +++ /dev/null @@ -1,100 +0,0 @@ -# 迭代 34 卡③ 独立验收审计(2026-10-22) - -被审对象:实现报告 `reports/iter34-实现报告-2026-10-22.md`、需求卡片 `reports/需求卡片-敌人表深度带换代批-2026-10-21.md`、工作树 `D:\code\g-plan2-iter34-enemy`(分支 `iter34-enemy-band`,HEAD `7bb41891`,基线 `603eba31`)。 - -审计方式:只读代码、测试、卡片与 diff。所有结论以审计人自己运行的输出为准,不采信报告「已全绿」的表述。审计期间为复现撤销证明注入的变异,均用单文件 `git checkout -- ` 还原并复跑确认,工作树结束时无改动。 - -## 审计结论 - -不通过:缺漏 4 条(V7 无覆盖行、fv24 尾缀挂错需求点;V9/fv26 覆盖行缺失;S15 `boss_name` 换代无断言;V6①/fv21 撤销证明按记录无法变红),另有描述不符 4 条;其余对照行通过。 - -## 一、对照表逐行核对 - -核对方法:按报告 §2.2 的 file:line 逐条读断言文本,grep 尾缀〔fv#〕确认可定位,再沿断言回溯到改动点入口(`resolve_enemy_rowset`/`EnemyTable.validate`/`cross_validate`/生产表读数),并在工作树实跑对应套件确认该断言在跑。 - -| fv | 需求点 | 测试存在(file:line) | 尾缀可 grep | 真实入口 | 结论 | -|---|---|---|---|---|---| -| fv1 | V1① 带命中偏难 | `config_db_resolver_test.gd:58`、`:61` | 是 | `resolve_enemy_rowset(1,4/5,"battle")` → `band_of` | 通过 | -| fv2 | V1② 行序保序 | `config_db_resolver_test.gd:67`、`:71` | 是 | 同上,断言行集顺序 | 通过 | -| fv3 | V1③ 顶带 clamp+告警去重 | `config_db_resolver_test.gd:78`、`:84`、`:87` | 是 | 同上,D=99 与重复请求 | 通过 | -| fv4 | V1④ row_kind 路由 | `config_db_resolver_test.gd:93` | 是 | 同 D 传 battle/boss | 通过 | -| fv5 | V1⑤ 越域与非法行类 | `config_db_resolver_test.gd:101`、`:104`、`:107` | 是 | `resolve_enemy_rowset(0,…)`/`(1,-1,…)`/`(1,0,"elite")` | 通过 | -| fv6 | V2① 生产表校验零错误 | `config_db_test.gd:14` | 是 | `ConfigDB.load_tables` → `EnemyTable.validate` | 通过 | -| fv7 | V2② 非法 row_kind 被拒 | `config_db_test.gd:256` | 是 | `validate()` 非法枚举夹具 | 通过 | -| fv8 | V2③ dup_key 新键拦截 | `config_db_test.gd:235` | 是 | `validate()` 同带同敌种二行夹具 | 通过 | -| fv9/fv20 | V2④、V5③ 同带隔断 | `config_db_test.gd:225`(一条断言两个尾缀) | 是 | `validate()` 隔断夹具 | 通过 | -| fv10 | V2⑤ 锚行与 Boss 行集 | `config_db_test.gd:265`、`:273` | 是 | `validate()` 缺锚/缺 Boss 夹具 | 通过 | -| fv11 | V3① 20 行计数与键序 | `smoke_all.gd:2106` | 是 | 加载 `enemy.tres` 全表逐行比对 | 通过 | -| fv12/fv25 | V3②、V8 hobgoblin 留任 | `smoke_all.gd:2114`、`:2116` | 是 | 同 fv11 读表面+表注释 | 通过 | -| fv13 | V3③ 三 Boss 行就位 | `smoke_all.gd:2132` | 是 | 同 fv11 读表面 | 通过 | -| fv14 | V3④ 值列零变更 | 无断言(件核):`git diff 603eba31..7bb41891 -- game/data/tables/enemy.tres` | 无(卡片约定件核) | diff 去键列后逐值列比对 | 通过(去键列后值列逐字节一致) | -| fv15 | V4① 同带双 boss 报错 | `config_db_cross_validator_test.gd:137` | 是 | `cross_validate()` 双 Boss 共带夹具 | 通过 | -| fv16 | V4② 顶 clamp 消解报错 | `config_db_cross_validator_test.gd:142` | 是 | `cross_validate()` Boss 深度 10>顶带 9 | 通过 | -| fv17 | V4③ 单终点零误报 | `config_db_cross_validator_test.gd:132` | 是 | `cross_validate()` 合成单关夹具 | 例外:夹具只有 level 1(`display_name:"夹具关"`),报告写「现役三关夹具」不实;现役三关零误报由 fv6 兜住 | -| fv18 | V5① 混编行序 | `smoke_all.gd:4815` | 是 | 读 (2,battle,5) 行集顺序 | 通过 | -| fv19 | V5② 骰和 ≤4 | `smoke_all.gd:4820` | 是 | 读同行集静态求和 | 例外:断言是静态求和,放宽它本身不会红;能变红的是同段未编号的运行期守卫断言 | -| fv21 | V6① 首遇带数据驱动 | `smoke_all.gd:2282`(定位助手 `:3516`) | 是 | `_first_encounter_band` 扫表+行集读数 | 通过(撤销证明不成立,见第四节第 4 条) | -| fv22 | V6② 巨蜘蛛首遇带固定 2 | `smoke_all.gd:4801` | 是 | 扫表定位+行集读数 | 通过 | -| fv23 | V6③ L1/L3 首遇带固定 1 | `smoke_all.gd:3473`、`:4859` | 是 | 同上 | 通过 | -| fv24 | 报告写作「Boss 行集与毒吐投放面」 | `smoke_all.gd:4281`、`:4291`、`:4807`、`:4865` | 是 | 扫表定位+Boss 行集 | 缺漏:卡片 V7 要求「S 清单 17 行逐条换代+清单件核」,这 4 条断言不是该需求点的验证;报告无 V7 行,也无 S1–S16 逐条勾销表 | -| fv25 | V8 缺口登记随行 | `smoke_all.gd:2116`(与 fv12 共用行) | 是 | 表文件注释在位 | 通过 | -| —(V9/fv26) | V9 反哺点五落点件核 | 报告 §2.2 无此行 | — | 应为 `reports/iter34-反哺点-2026-10-22.md` 五条落点件核 | 缺漏:反哺点文件已有 ①–⑤ 五节,但对照表没有 fv26 覆盖行 | - -需求点覆盖对照:V1①–⑤=fv1–fv5,V2①–⑤=fv6–fv10,V3①–④=fv11–fv14,V4①–③=fv15–fv17,V5①–③=fv18–fv20,V6①–③=fv21–fv23,V8=fv25;V7 与 V9 没有对应覆盖行(见上表两处缺漏)。 - -## 二、撤销证明抽查 - -抽查 6 项(报告 §2.3 七条中取 5 条复现,另核 1 条不可复现)。每条流程:改一个文件注入 → 跑报告指明的最窄命令 → 确认对应断言变红 → `git checkout -- <单文件>` 还原 → 复跑回绿。 - -| 变异 | 复现结果 | 红输出首行 | 还原结论 | -|---|---|---|---| -| 1. `EnemyTable.band_of` 判据 `>=` 改 `<=`(反向 floor) | 已复现 | `[FAIL] 带命中取偏难一侧:请求深度 4 命中带 5(逐字不回带 0)〔fv1〕`;连带〔fv1〕第二条与〔fv3〕两条 | `git checkout -- game/data/schemas/enemy_table.gd`;resolver 套件 14 项全绿 | -| 2. `validate` 的 row_kind 枚举判据前置 `if false and` | 已复现 | `[FAIL] 非法 row_kind 被拒(封闭枚举 {battle, boss})〔fv7〕`(1/29 项失败) | 同文件 checkout;config_db_test 29 项全绿 | -| 3. 删除跨表「Boss 同带」判据 | 已复现 | `[FAIL] 同关双终点共带报 rowset_rule〔fv15〕` | `git checkout -- game/core/config_db_cross_validator.gd`;跨表套件 7 项全绿 | -| 4. 顶带消解判据短路 `if false` | 已复现 | `[FAIL] Boss 节点越顶带报 level_node_dangling〔fv16〕` | 同文件 checkout;7 项全绿 | -| 5. hobgoblin 与带 2 行互换带键(报告第 7 项第二注入法) | 已复现 | 冒烟首红〔fv11〕「敌人表 20 行且键序=迁移表序…」,随后〔fv12〕〔fv25〕红 | `git checkout -- game/data/tables/enemy.tres`;冒烟 1161 项检查 0 失败 | -| 6. hobgoblin 直接改到带 2(报告第 7 项第一注入法) | 已复现,但拦截者与报告所写不同 | `ERROR: [ConfigDB/level_node_dangling] level 1 battle 节点深度 3 高于 (level 1, battle) 顶带键 2——顶 clamp 消解`;随后〔fv6〕红,套件 2/2 项失败即止 | 同文件 checkout;config_db_test 29 项全绿 | -| 7. V6①「撤销扫表定位、写回写死坐标」 | 未复现(按记录设计不成立) | — | 未注入:助手 `_first_encounter_band`(`smoke_all.gd:3516`)返回值与旧写死值同为 L1=2/L2=4/L3=0,写回后断言值不变、不会红;要让 fv21 变红只能改数据把首遇行移带 | - -实际运行命令(均在工作树,`PYTHONIOENCODING=utf-8`,Godot 4.7.2 console 版 headless,一次只跑一个进程): - -- `--script res://tests/unit/core/config_db_resolver_test.gd`(多轮,含注入与还原后复跑) -- `--script res://tests/unit/data_config/config_db_test.gd`(多轮) -- `--script res://tests/unit/core/config_db_cross_validator_test.gd`(多轮) -- `--script res://tests/run_all.gd`(全量单元 35 套件) -- `--script res://smoke/smoke_all.gd`(1161 项检查 0 失败,多轮) -- `git diff`、`git status`、`git checkout -- <单文件>` - -补充核验:把 `battle_narrator.gd:76` 的 `"boss"` 改为 `"battle"` 后,35 个单元套件与整轮冒烟仍全绿,说明该改动点没有被任何断言拦住(见第四节缺漏 3)。 - -## 三、测试削弱检查 - -检查范围:`git diff 603eba31..7bb41891 -- game/tests game/smoke`(删 173 行、增 418 行)。逐项核对删除、阈值、扫描面与入口层级: - -1. `config_db_resolver_test.gd` 删除 `_test_ceil_progress_key`(board_gen ceil 3 条断言):属卡片 §8.2 旧断言预声明 S11「换代为 V1 带寻址语义断言」;board_gen 的 ceil/越顶/去重仍由 `config_db_test.gd:30-82` 覆盖,未失效。 -2. gp 掉落断言由硬编码 `[2,4]` 改为读 `shop.tres` Boss 档区间+关级加值:报告 §2.4 第 2 条已申报;判据仍比对结算输出,强度不低于原式。 -3. `skill_unlock_test.gd` Boss 用例 `spawn(2,9)` 补显式 `"boss"`:报告 §2.4 第 3 条已申报。 -4. 冒烟多处补 `node_type:"boss"`/补行类参数:报告 §2.4 第 1 条已申报。 -5. 未发现「真实入口换低层直调」:fv1–fv5 仍走 `resolve_enemy_rowset`,fv6–fv10 走 `EnemyTable.validate`(经加载),fv15–fv17 走 `cross_validate`,冒烟断言走 `_db.enemy.rows` 与 `spawn`。 -6. 检查数量:冒烟 1156→1161,单元 35 套件不变;无净收缩。其余删除行均为按 S 清单把旧坐标断言改写为带键断言,语义保持。 -7. 未发现未申报的阈值放宽或扫描面缩小。 - -结论:未发现未申报的测试削弱。另有一处计数描述不符:报告称「35 条断言带 fv 尾缀」,实测带尾缀行 35 行,其中 `smoke_all.gd:969` 是注释,断言为 34 条。 - -## 四、缺漏与例外 - -缺漏: - -1. **V7 无覆盖行、fv24 尾缀挂错需求点**。位置:报告 §2.2 表(无 V7 行);卡片 §8.2 V7 要求「S 清单 17 行逐条换代+实现报告逐条勾销」。事实:fv24 的 4 条断言(`smoke_all.gd:4281`/`:4291`/`:4807`/`:4865`)验证的是 Boss 行集与毒吐投放面,不是 S 清单完整性;报告也没有 S1–S16 逐条勾销表。建议:补 V7 覆盖行与逐条勾销表,或经卡片所有者同意把 V7 的验收方式改写为实际断言。 -2. **V9/fv26 覆盖行缺失**。位置:报告 §2.2 表;卡片 §8.2 V9。事实:`reports/iter34-反哺点-2026-10-22.md` 已有 ①–⑤ 五节落点,实物在,但对照表没有 fv26 件核行,无法按表逐行核对。建议:补一行「V9|五条落点各一|件核:反哺点文件 ①–⑤|非行为改动|通过」。 -3. **S15 `boss_name` 换代无断言**。位置:`game/presentation/battle_narrator.gd:76`;卡片 §8.1 S15、§8.2 V7。事实:把该处 `"boss"` 改成 `"battle"` 后,35 个单元套件与整轮冒烟 1161 项全绿;`battle_narrator_test.gd` 从未调用 `boss_name`。建议:补一条走真实入口的单测(构造 Boss 节点,断言叙述名取 boss 行集名),或在报告 §2.4 如实声明该处无断言并说明理由。 -4. **V6①/fv21 撤销证明不成立**。位置:卡片 §8.2 V6① 撤销证明、报告 fv21 行。事实:按记录「撤销扫表定位、写回写死坐标」还原后,助手返回值与写死值相同(L1=2/L2=4/L3=0),断言不会红;该撤销证明无法区分「数据驱动」与「写死」。建议:改为数据侧变异(把首遇行移到别的带,断言应取键失配变红)或如实标注撤销边界。 - -例外(描述不符,不影响其余覆盖): - -5. **fv17 夹具描述**。位置:`config_db_cross_validator_test.gd:128-132`;报告 §2.2 fv17 行写「现役三关夹具」。事实:夹具是 level 1 合成单关(`display_name:"夹具关"`);现役三关零误报由 fv6(生产表加载零 error)覆盖。建议:改报告措辞,或补现役三关夹具。 -6. **fv19 撤销归属**。位置:`smoke_all.gd:4820`。事实:静态求和断言被放宽不会红;真正能红的是同段未编号的运行期守卫断言(「两行各定值 3(总 6>4)spawn 被拒」)。建议:报告撤销证明改写为「改 `ROWSET_INSTANCE_MAX` 守卫 → 守卫断言红」。 -7. **§2.3 第 7 项拦截边界描述有误**。位置:报告 §2.3 注①。事实:hobgoblin 直接改到带 2 时,首条红是跨表 `level_node_dangling`(战斗节点深度 3 无带可落),不是「同带相邻规则」;同带相邻未触发,是因为 hobgoblin 紧接在 bugbear 带 2 行之后、文件内相邻。建议:按实际首红改写。 -8. **断言计数**。位置:报告 §2.2 覆盖说明「共 35 条断言带 fv 尾缀」。事实:带尾缀行 35 行,其中 1 行为注释(`smoke_all.gd:969`),断言 34 条。建议:改为 34 条。 - -审计结束时工作树 `git status --short` 为空,HEAD 仍为 `7bb41891`;除本报告外未新增或修改任何文件。 diff --git a/reports/iter34-验收审计-复审-2026-10-22.md b/reports/iter34-验收审计-复审-2026-10-22.md deleted file mode 100644 index c7ef9716..00000000 --- a/reports/iter34-验收审计-复审-2026-10-22.md +++ /dev/null @@ -1,98 +0,0 @@ -# 迭代 34 卡③ 独立验收审计·第二轮复审(2026-10-22) - -- 被审 HEAD:`68009b4e`(`iter34-enemy-band`,基线 `603eba31`);第一轮被审 HEAD `7bb41891` -- 被审对象:实现报告 `reports/iter34-实现报告-2026-10-22.md`(§2.2/§2.2.1/§2.3/§3)、第一轮审计报告 `reports/iter34-验收审计-2026-10-22.md`、修补 diff `7bb41891..68009b4e`、卡片 `reports/需求卡片-敌人表深度带换代批-2026-10-21.md` -- 审计方式:只读代码、测试、卡片与 diff;所有结论以审计人自己运行的命令输出为准,不采信报告「实测/已全绿」表述。为复现撤销证明注入的变异一律单文件 `git checkout --` 还原并复跑确认;不跑全量闸(`tools/run_all_gates.sh` 留给实现会话在最终提交上跑)。审计结束时工作树 `git status --short` 为空(本报告除外)。 - -## 结论速览 - -**A–F 六项全部通过;总结论:通过。** 第一轮 4 条缺漏与 4 条描述不符全部真闭合,本次修补未引入测试削弱,无阻塞遗留例外。 - ---- - -## A. 缺漏 1(V7 覆盖行+17 行勾销表)——通过 - -依据: - -1. 报告 §2.2 已新增 fv24 覆盖行,需求点改写为「V7 S 清单 17 行逐条换代」,撤销列指向「逐条勾销见 §2.2.1」。 -2. 报告 §2.2.1 存在 17 行勾销表:S1–S9、**S10a/S10b 分列**、S11–S16,逐行给出「现工作树位置/换代内容/勾销凭据」,S11–S13 归单测面、S15 归消费面,口径与卡片 §8.1 一致。 -3. 按任务要求抽查 6 行(逐行实读代码确认位置与换代内容属实): - -| 抽查 | 报告所记位置 | 实读确认 | -|---|---|---| -| S1 | `smoke_all.gd:3168`/:3491/:4499 | 三处均为 `resolve_enemy_rowset(1, …, "battle")[0]` 三参化,且带「行集首行(…battle 键域)」注释,属实 | -| S6 | `smoke_all.gd:974-984`/:4781-4789/:5256-5258 | 三处 `en_keys["%d_%s_%d" % [level_index, row_kind, depth_band]]`/`"%d_boss_9"` 查 boss 键域,属实 | -| S8 | `smoke_all.gd:4262-4306` | `_first_encounter_band(2,"giant_spider")` 取首遇带、判据改 `depth_band > spider_lead_band`、`expect_pools` 键串含 row_kind(如 `2_battle_5_giant_spider`),属实 | -| S10a | `smoke_all.gd:2024`/:2674/:2699…;`damage_probe.gd:95` | 抽 3 行均为 `"node_type": "boss"` 直呼;另用脚本核了表中全部 18 个 `smoke_all.gd` 行号,每个都含 `node_type": "boss"`,属实 | -| S14 | `skill_rhythm_probe.gd:17`;`damage_probe.gd:269`/:422 | 三处均显式传 row_kind(`"battle"`/`t`/`kind`),属实 | -| S16 | `smoke_all.gd:3491`(断言 :3496) | `erow2 = _db.resolve_enemy_rowset(1, int(depth_v), "battle")[0]`,下行 `scan_bad == 0` 断言,属实 | - -另核 S2/S3/S4/S5/S7/S9 的行号指向(4795/4802/4810/4852/4860、2263/2279/2301、3460-3474),均与断言实际位置对得上,未发现空指或错指。 - -## B. 缺漏 2(V9/fv26 覆盖行)——通过 - -- 报告 §2.2 新增 fv26 行:「V9 反哺点五落点(非行为改动)|`reports/iter34-反哺点-2026-10-22.md` ①–⑤|件核」,与卡片 V9「§6 五条每条有落点」对应。 -- `reports/iter34-反哺点-2026-10-22.md` 实读确有 ①–⑤ 五节(① 敌人系统.md 记法与措辞、② 关卡节点地图.md :146、③ 关卡节点图生成.md 产出规格、④ 待办清单.md 状态回写、⑤ roadmap.md 迭代行),每节均写「文件与位置/改成/依据」。 - -## C. 缺漏 3(S15 `boss_name` 断言)——通过 - -- 用例位置:`game/tests/unit/presentation/battle_narrator_test.gd:59-82`,`_test_boss_name_uses_boss_rowset()` 由 `_init` 调用。 -- 真实入口成立:用例加载生产表后调 `NarratorScript.boss_name(runner, db)`(`battle_narrator.gd:72`),函数内走 `db.resolve_enemy_rowset(..., "boss")`(`:76`);生产调用点 `presentation/main.gd:1331` 同款。runner 替身只提供 `boss_name` 实际读取的 `level_row`/`current_node()` 两项,与 `core/level_runner.gd` 的同名面一致。 -- 期望值由数据读出:`boss_depth` 扫表取 L1 boss 行 `depth_band`,`boss_expect`/`battle_name` 分别由 boss/battle 行集首行 `display_name` 读出,无写死字面量;另有一条「boss 名 ≠ 同深度请求的战斗行集名」判别断言。 -- 自跑(命令与首行输出): - - `PYTHONIOENCODING=utf-8 "" --headless --path . --script res://tests/unit/presentation/battle_narrator_test.gd` - → `[battle_narrator_test] all green (13 checks)`,EXIT=0 - - 撤销注入:把 `battle_narrator.gd:76` 的 `"boss"` 改 `"battle"` 后重跑 - → `[FAIL] Boss 名取自 Boss 行集首行(期望「食人魔酋长」实际「大地精」)〔fv24〕`;`[battle_narrator_test] 1/13 项失败`,EXIT=1 - - `git checkout -- game/presentation/battle_narrator.gd` 后复跑 → `[battle_narrator_test] all green (13 checks)`,EXIT=0 -- 与报告 §2.3 第 9 项所记(红断言原文、13 项回绿)逐字相符。 - -## D. 缺漏 4(V6①/fv21 撤销证明)——通过(实测与报告一致) - -实测流程与结果: - -1. 数据侧变异:`game/data/tables/enemy.tres` 的 (level_index 1, row_kind battle, depth_band 2, bugbear) 行 `depth_band` 2→1,跑 `res://smoke/smoke_all.gd` - → `=== 冒烟完成:1161 项检查,6 项失败 ===`(EXIT=1)。〔fv21〕**不在**失败列表(首遇带随数据变为 1、锚定行随迁——数据驱动正证成立)。6 项红为:〔fv11〕20 行键序、〔fv23〕熊地精首遇带固定 1、全深度×12 种子 spawn 扫描、「二十行九种池模板」、「先手击杀→摇定技能作废」、「行集内每个战斗/Boss 节点…精确行集(缺失 1)」。报告所记「共 6 项红」属实。 -2. 叠加写死:在 `smoke/smoke_all.gd` 的 `_first_encounter_band` 首行插入 `return 2`,重跑冒烟 - → 首条红即 `[FAIL] 熊地精首遇带(1,battle,2)降档锚定(hp 22 · ac 2 · 池 attack6/charge2/heavy2 零改动——迭代 24 N1;首遇带数据驱动定位)〔fv21〕`;`=== 冒烟完成:1161 项检查,9 项失败 ===`(EXIT=1)。报告所记「共 9 项红」属实。 -3. `git checkout -- game/data/tables/enemy.tres game/smoke/smoke_all.gd` 还原,复跑 - → `=== 冒烟完成:1161 项检查,0 项失败 ===`(EXIT=0)。 - -结论:该撤销证明现在能真正区分「数据驱动定位」与「写死坐标」——单跑数据变异〔fv21〕绿、叠加写死才红,报告 §2.3 第 8 项的记述与实测逐项一致。 - -## E. 描述不符 4 条——通过(4/4 已按实改写) - -1. **fv17 夹具措辞**:`config_db_cross_validator_test.gd:95-116` 的 `_loaded()` 只构造 `level_index: 1`、`display_name: "夹具关"` 的合成单关夹具;fv17(:132)在该夹具上断言零误报。报告已改为「level 1 合成单关夹具;现役三关零误报由 fv6 兜住」,与实读一致。**通过**。 -2. **fv19 撤销归属**:`smoke_all.gd:4810-4820` 的静态求和(首遇混编数据 Σ=4,阈值 `<= 4`)放宽阈值不会红;真正能红的是同段运行期守卫(`:4891-4914` 注入 (1,battle,6) 两行各 3 只,`:4909` 断言 `not over_sys.spawn(...)`,由 `ROWSET_INSTANCE_MAX` 守卫兜住)。报告已改为「改运行期守卫 → 守卫断言红;静态求和本身放宽不会红」,归属正确。**通过**。 -3. **§2.3 第 7 项拦截首红**(可选复现已做):把 hobgoblin 行 `depth_band` 3→2 后跑 `res://tests/unit/data_config/config_db_test.gd`,首条错误为 - `ERROR: [ConfigDB/level_node_dangling] level 1 battle 节点深度 3 高于 (level 1, battle) 顶带键 2——顶 clamp 消解` - 随后 `[FAIL] 全部表加载 + 校验通过(含跨表护栏零 error 诊断)〔fv6〕`,EXIT=1;`git checkout --` 还原。与报告改写后的「跨表 level_node_dangling 首红、同带相邻未触发」一致。**通过**。 -4. **断言计数**:`grep -rn "〔fv[0-9]*〕" tests/ smoke/ --include=*.gd` 得 37 行;逐行核对,`smoke_all.gd:969` 为注释(`# 同带多行合法,带键聚合天然兼容〔fv24〕)`),其余 36 行均为断言行 → 「36 条断言+1 行注释」属实;fv14/fv26 按卡片约定走件核,代码侧无尾缀,覆盖说明自洽。**通过**。 - -## F. 测试削弱检查——通过 - -检查范围:`git diff 603eba31..68009b4e -- game/tests game/smoke`(8 个文件,+449/−166)与本次修补 `git diff 7bb41891..68009b4e`。 - -1. **本次修补未碰产品代码与既有断言**:`7bb41891..68009b4e` 仅 `battle_narrator_test.gd` 38 行新增(0 删除)+两份报告;新增用例为纯增量,无删断言、无阈值放宽、无扫描面缩小。 -2. **全量 diff 的删除行均属卡片 §8.2「旧断言预声明」允许的 S1–S16 改写**:删除面集中在 `config_db_resolver_test.gd`(S11 换代)、`config_db_test.gd`(S12 夹具键改名)、`config_db_cross_validator_test.gd`(S13)、`smoke_all.gd`(S1–S10)、两份探针(S14);`skill_unlock_test.gd` 的 `spawn(2,9)` 补显式 `"boss"`、冒烟 gp 掉落断言改读 `shop.tres` Boss 档区间+关级加值,均在报告 §2.4 第 1–3 条申报。 -3. **无阈值放宽 / 无扫描面缩小**:diff 中没有 seed/`range()` 扫描规模改动;第一关曲线阈值 `≤ 2.5`、混编 `≤ 4`、首遇 `unit_count=2`/固定 1 等判据原样保留;gp 掉落断言由硬编码 `[2,4]` 改为数据驱动 `Boss 档 [6,10] + 关级加值`(因 S10a 把该战改为 boss 语义,掉落档合法切换),判据仍比对结算输出,强度不降。 -4. **真实入口未换下层直调**:fv1–fv5 走 `resolve_enemy_rowset`,fv6–fv10 走加载后的 `EnemyTable.validate`,fv15–fv17 走 `cross_validate`,fv18–fv23 走冒烟表读取+`spawn`,S15 走 `boss_name` → `resolve_enemy_rowset(..., "boss")`。 -5. **数量面**:冒烟 1156→1161(本卡 +5),单元套件 35 个不变;`battle_narrator_test` 检查数 11→13(新增 2 条)。无净收缩。 - -结论:未发现未申报的测试削弱,本次修补为纯增量。 - ---- - -## 遗留例外/观察(不阻塞) - -1. 第一轮审计报告第三节所记 diff 行数「删 173 行、增 418 行」与实测 `git diff --numstat 603eba31..7bb41891 -- game/tests game/smoke`(+411/−166)不一致,属第一轮报告的计数笔误(不影响其结论,也不属实现报告内容);本轮结论以实测为准。 -2. 新单测在解析 L1 battle 行集时(同深度请求落在 Boss 深度 4)会触发一次 `resolve_depth_clamp` 警告——这是顶带 clamp 的预期语义,非失败;用例仍 13 项全绿。 - -## 运行命令清单(均为工作树 `game/` 下、Godot 4.7.2 console 版、`PYTHONIOENCODING=utf-8`、一次一个进程) - -- `--headless --path . --script res://tests/unit/presentation/battle_narrator_test.gd`(注入前后与还原后共 3 轮) -- `--headless --path . --script res://smoke/smoke_all.gd`(数据变异/叠加写死/还原后共 3 轮) -- `--headless --path . --script res://tests/unit/data_config/config_db_test.gd`(hobgoblin 错带复现 1 轮) -- `git diff 603eba31..68009b4e -- game/tests game/smoke`、`git diff --numstat`、`grep -rn "〔fv[0-9]*〕"`、`git checkout -- <单文件>`、`git status --short` - -审计结束时工作树除本报告外无改动,HEAD=`68009b4e`。 diff --git a/reports/iter35-卡2-停机呈报-2026-10-24.md b/reports/iter35-卡2-停机呈报-2026-10-24.md deleted file mode 100644 index b44853ed..00000000 --- a/reports/iter35-卡2-停机呈报-2026-10-24.md +++ /dev/null @@ -1,57 +0,0 @@ -# 迭代 35 卡② 停机呈报(实现段零裁断——退回上游) - -- 日期:2026-10-24 -- 触发:`/iter 35` 第 4 步逐卡推进——卡① 已开工并实现,卡② 开 T1 前核 §0 前置,命中卡片自述硬交付序列 -- 批性质:代码实现批;本呈报只移交缺陷,不改卡片、不改 `design/`、不改待办清单 -- 本卡改动:**零**(未动 `level_table.gd`/`level.tres`/`level_runner.gd` 任何消费侧文件) - -## 一、现状差在哪 - -卡② `reports/需求卡片-节点图消费侧换代批-2026-10-21.md` §0「硬交付序列」明文: - -> 敌人深度带数据对生成图深度域的覆盖……归口=卡③ §5 已登记的「生成图深度扩面覆盖问题」,**尚未立卡(2026-10-24 实况登记)**……未覆盖时覆盖断言(§8.1 S5)红、遭遇解析顶 clamp 消解,**本卡不得单独合入**。 - -待办清单 `待办清单.md:143` 同口径登记:覆盖批的立卡要件(敌侧每 (关, row_kind) 顶带键 ≥ 该关最大生成深度+Boss 带键可互异+板侧 `board_gen` 键域覆盖全域)尚未成为任何一张卡片,roadmap 也没有该批的迭代行。 - -卡① 已落地的参数档深度域(`game/data/tables/level_gen_params.tres`)= L1 0–6/L2 0–9/L3 0–13;敌侧带顶仍是 L1=3/L2=L3=7、Boss 各关单带 4/9/9,板侧 `board_gen` 精确行同域缺口未变。 - -## 二、建议做什么 - -卡② 停工退回上游,按既定归口先走「生成图深度扩面覆盖批」的登记→写卡→编排三段,该批落地后再重编卡②。 - -预期效果:卡② 的 S5 覆盖断言(V11)有绿灯前提,切源后起关不再触发 `EnemyTable.band_of` 返 -1 的加载致命(`game/data/schemas/enemy_table.gd:157-161`、`game/core/config_db_cross_validator.gd:94-106`)。 - -会动到的面:`game/data/tables/enemy.tres` 深度带键域与 Boss 行集、`game/data/tables/board_gen.tres` 的 `node_depth` 键域——全是数值表改动,需用户发起(AGENTS.md 数值单点+MC 审批红线),不属本实现批职权。 - -本迭代已交付面:卡① 生成器本体+参数档数据表+产出侧自证+断言批(见 `reports/iter35-卡①-实现报告-2026-10-24.md`)。 - -## 三、为什么选这个做法 - -备选与淘汰依据: - -1. **本批顺带补敌/板覆盖数据**——淘汰:卡② §5「不做」与卡① §5「不做」都明写覆盖归卡③/后续表批;实现段自扩数值域=越范围,且数值改动需用户发令。 -2. **卡② 只交 T1(加载校验换代)不合切源**——淘汰:T1 退役旧判据与模板池校验分支后,`level.tres` 仍携 15 张模板,中间态必红(卡② §7 估时注记「T2+T3 须同轮落地,拆开中间态冒烟必红」),交出去的是一半不能验收的卡。 -3. **等覆盖批、卡② 整卡停工**(本呈报建议案)——代价=迭代 35 只交卡①,与 2026-10-24 上午停机呈报的「读法乙」一致,用户当时已裁「整体停工」并按此重锚两卡。 - -## 四、做完怎么验证 - -覆盖批的验收面已由卡② 预写死:S5/V11 断言=生成图逐 battle/boss 节点 `band_of` 非 -1(禁顶 clamp)+多终点 Boss 解析带键两两互异+板侧解析命中非空,扫描面=三关参数档×种子(卡② §8 V11)。该批落地后卡② 重编,这些断言即其开工门。 - -把握不足处:L2/L3 多终点 Boss 带键「可互异」需要敌表 Boss 行集从单带扩成按终点深度分带——具体带切法属卡③ 契约 5 的续域,覆盖批写卡时须一并裁。 - -## 五、待办清单状态回退与缺口补记拟稿(落笔归上游段/用户) - -`待办清单.md:149` 现状态行:`文档设计已闭合+已写卡+已编迭代 34+已编迭代 35` - -拟改为: - -``` -**状态**:文档设计已闭合+已写卡+已编迭代 34+已编迭代 35;迭代 35 卡① 已实现待合回(2026-10-24);卡② 停工退回——覆盖批未立卡,其 §0 硬交付序列未满足,须「生成图深度扩面覆盖批」落地后重编 -``` - -缺口补记(拟入 06 条目,替换现第 143 行缺口段的「迭代 35 停机退回」表述): - -``` -**缺口状态更新(2026-10-24 迭代 35 实现批)**:卡② §0 硬交付序列仍未满足——覆盖批未立卡,卡② 停工退回上游;卡① 不触覆盖域,已在本迭代实现(生成器产出深度域 L1 0–6/L2 0–9/L3 0–13 实测坐实)。 -立卡要件不变:敌侧每 (关, row_kind) 顶带键 ≥ 该关最大生成深度+Boss 带键可互异;板侧 board_gen 键域覆盖生成深度全域;数值 provisional。 -``` diff --git a/reports/iter35-卡①-代拍板-2026-10-24.md b/reports/iter35-卡①-代拍板-2026-10-24.md deleted file mode 100644 index cc65aa95..00000000 --- a/reports/iter35-卡①-代拍板-2026-10-24.md +++ /dev/null @@ -1,24 +0,0 @@ -# 迭代 35 卡① 代拍板追认单(节点图生成器核心批) - -- 日期:2026-10-24 -- 依据:`reports/需求卡片-节点图生成器核心批-2026-10-21.md` §10 代拍板白名单六条——授权发生在卡片审问阶段,本批只执行预授权并留痕 -- 待用户动作:追认或推翻。推翻任一条即回写对应实现面(不改设计文档) - -| # | 卡片白名单条目 | 本批拍定值 | 落点 | -|---|---|---|---| -| ① | RNG 纯流面机制细节 | `pure_stream(seed_val: int, scope: int) -> RandomNumberGenerator`;派生式复用 `_mix64(SINGLE_SALT ^ (\|seed_val\| + scope × GOLDEN_RATIO))`;scope 分域取**单流多次取数**(`SCOPE_STRUCTURE = 0` 一个域,内部按取数序推进 RNG 状态);不推 `_counter`、不落事件日志、不读 `_master_seed`;负种子按 `absi` 归一、`INT64_MIN` 断言拒 | `game/core/rng_service.gd` pure_stream;断言〔fv3〕〔fv10〕 | -| ② | 「支路」度量口径 | 支路互斥段 = 自出边起的可达集,减去同岔口其余出边可达集的并集(即「至再汇合/终点止」的独占段);类型构成按该独占段的**多重集**(类型名排序数组)比较;「每支路至少一个别的支路没有的类型」判据 = 该独占段内存在一个类型不出现在任何其他支路的独占段 | `node_map_generator.gd` `_check_fork_rules`;断言〔fv5〕 | -| ③ | 生成器内持有结构与句柄赋值序 | 图持有 = `_pool`(Array[Dictionary])+`_by_id`;节点字段恰 `id/type/depth/next`;id 赋值序 = 入口 → 主链段自浅到深 → 各区域自终点倒序回填 → 侧链岔口;`generate` 返回 `{nodes, entries, terminals}` | 同上 `_build`/`_mk`;读面对齐 §7 接口块 | -| ④ | 参数档 .tres 行键形态与 provisional 微调 | `rows: Array[Dictionary]`,键 = `level_index`,字段键集 = `ROW_KEYS` 十二键闭集(含 `fork_ways_min/max`);三行取卡片 §8.1 表原值(layers 6/9/13、branch_density .30/.50/.70、配比三档、entry 1/2/2、exit 1/2/3、battle_floor 4/6/8),未做域内微调 | `game/data/schemas/level_gen_params.gd`、`game/data/tables/level_gen_params.tres`;断言〔fv2〕 | -| ⑤ | 长图算法具体步骤(HOW) | 单出口图(L1)= 入口→定长链→商店→Boss,链上挂侧链岔口;多出口图(L2/L3)= 入口→主链段→**归属大岔口**(主链段末节点切 e 条区域)→各区域单调链(区域内禁再扩岔);岔口侧链与主去路同深度、汇入主去路的后继(保「深度=最长路径步数」不变量);相邻两层禁同时扩岔(否则上层支路独占段吞掉下层侧链,破契约 5③);分支密度实际生效数 = min(round(bd×层高), 可用锚点数)——种子 42 实测侧支节点数 L1=4/L2=4/L3=3(总节点 11/19/29 减无岔口基线 7/15/26),provisional 期的实现自由面,定值批若要「十几次决策点」须放宽锚点几何) | `node_map_generator.gd` `_build`/`_add_forks` | -| ⑥ | 提交切分与分支命名 | 分支 `iter35`(单工作区 `../g-plan2-iter35`);提交三笔=卡① 实现(含断言与 manifest)/fv4 判据三分码修订/致命面体例修正+断言判别力补强 | git log(63a6ea80、a039306f、16c776e9) | - -## 白名单外、本批自认为需要呈报的两件 - -1. **致命面形态修正**:卡片 §7 写「push_error+assert/既有 fatal 路径」。实测 assert 在 headless `--script` 下只中断当前调用链、`OS.crash` 会阻塞跑批且无法被断言消费——故按仓库既有体例(`config_db.gd:78-88` Core 置 `_fatal_flag`+push_error,退出决策归组合根 `main.gd:396 _fatal_screen`)实现:`generate` 不合格时置致命封印+断言+**零交付**(不返回任何图),消费侧(卡② 起关路径)按 `is_fatal()` 停机。契约 4 语义(无兜底图、无软回退、不合格不进玩法)不变,只是停机点从生成器内移到组合根。 -2. **调试面曾加一件方法 `generate_for_debug(params, seed)`**——独立验收审计判定其零调用方=死代码,**本批已删除**(断言读产出规格违例改走公有 `validate_output(graph, params)` 直调,能力验证面仍由卡片 §8 两分法的现役 DebugOverlay/DS_DEBUG 承载)。此条不再请求追认,仅作处置留痕。 - -## 遗留读数义务(实现报告同步在册) - -- 用户目检两条未做(登记不阻塞):三关生成图选路观感、生成规模随关卡变大可感知——呈现依赖卡②/卡④ 落地。 -- MC/对拍零跑:本批无数值改动、未涉核心结算,按红线不发起跑批。 diff --git a/reports/iter35-卡①-反哺点-2026-10-24.md b/reports/iter35-卡①-反哺点-2026-10-24.md deleted file mode 100644 index cf0e3cd9..00000000 --- a/reports/iter35-卡①-反哺点-2026-10-24.md +++ /dev/null @@ -1,22 +0,0 @@ -# 迭代 35 卡① 反哺点清单(节点图生成器核心批) - -- 日期:2026-10-24 -- 出处:`reports/需求卡片-节点图生成器核心批-2026-10-21.md` §6 关联文档同步面六条 + 实现中新发现一条设计面事实 -- 状态:待用户在会话里逐条裁决;裁决后由文档批写进 `design/`,本文件随之退役(doc-conventions §11 吸收即死) -- 依据代号沿卡片 §2:D1=关卡节点图生成.md v1.1,D2=关卡节点地图.md v2.1,D3=冒险结构.md - -| # | 文件与节 | 改成什么 | 依据 | -|---|---|---|---| -| ① | `design/gdd/systems/冒险循环/关卡节点图生成.md` 产出约束节(D1 :41-49 五条之后) | 增一条产出约束「多终点落在互异节点深度」;并在层高条目处成文「节点深度=自入口最长路径步数(多入口取距任一入口的最长路径)」 | 卡① 契约 2;D2 :119「定位信息只有两样(关卡序号+节点深度)」+D1 :96「多终点各配 Boss 行、走哪条路决定打谁」合取——同深度多终点在只有两样定位信息下不可区分;卡③ 契约 6 推论条款 A 的图侧兑现(卡③ §6 反哺点③ 派发) | -| ② | `design/gdd/systems/冒险循环/关卡节点地图.md` :65 | 「节点深度=从入口沿路走过来的步数」补注为「自入口最长路径步数」,并说明合流图上为何不能用最短路径 | 卡① §11 推导:最短路径赋值下合流/多入口节点可出现「出边目标深度 ≤ 源」,使无环判据(D1 :51 连线只朝前进方向)失效;实现侧断言 `node_map_generator_test`〔fv1〕已按最长路径口径写死 | -| ③ | `design/gdd/systems/冒险循环/冒险结构.md` :293 覆盖义务行 | 登记生成图实际深度域供覆盖核对:**L1 深度 0–6/L2 0–9/L3 0–13**(三档参数档 `layers` = 6/9/13 实测,2026-10-24 卡① 落地读数);覆盖义务措辞换代(精确行→敌侧带命中+板侧解析命中)仍归卡③ 反哺线,本条只补深度域数值 | D3 :293 覆盖义务+卡① §0 交付序列;`game/data/tables/level_gen_params.tres` 现值 | -| ④ | `design/gdd/ux/level-map.md` | 登记呈现面随动需求:多入口首跳候选、多终点标记、含岔口图与深度域 0–13 的画布自适应;本批未改任何呈现代码 | 卡① §6 表行(06 条目连带面②);落地归卡④/level-map 文档批 | -| ⑤ | `待办清单.md` 06「关卡节点图重构——可选节点太少」条目 | 状态推进为「卡① 已实现待合回;卡② 停工退回(覆盖批未立卡)」,并把第 143 行缺口段更新为「已实现卡① 深度域实测」版本——拟稿见 `reports/iter35-卡2-停机呈报-2026-10-24.md` 第五节 | 实现批对流程清单零改动(AGENTS.md 两类批红线),条目状态只由流水线各段推进 | -| ⑥ | `roadmap.md` 迭代 35 行 | 卡片列与状态换代:卡① 标实现批已交付(合回后改「已完成」)、卡② 标停工退回上游;顶部「当前位」三行随之更新 | 卡① §6 表行;roadmap 属文档批职权,本批只列点不落笔 | -| ⑦ | `design/gdd/systems/冒险循环/关卡节点图生成.md` 配比与摆放约束节(D1 :74-88) | 成文一条结构上限:出口个数 3 不只是调参表的取值域,而是摆放约束③「每支路至少一个别的支路没有的类型」的天花板——五类节点里商店与 Boss 每条支路必有,可供支路互斥的唯一类型只剩 battle/reward/rest 三类,故一条链上的岔口最多切出 3 条互异支路 | 实现实证:卡① 三档构型中,多终点图必须给每个归属区域指定互斥签名类型才能过支路互异断言(`node_map_generator.gd` REGION_SIGNATURES 三元素);D1 :107 出口 1–3 已锁范围与该推论一致,但文档未写理由 | - -## 与实现差异/缺口逐条自证 - -- ①②:文档只写「步数」未写「最长」,实现取最长路径赋值——差异已按契约 2 落码并写断言,属文档缺位。 -- ⑦:文档「岔路取舍·支路构成」条目在三终点情形下隐含唯一可行解(每区域独占一类),实现按此构型;文档若不加此句,后续调参批可能误配四出口。 -- 本批未触碰 GDD 任何条款文本,也未新增/修改 `design/` 与两份根流程清单。 diff --git a/reports/iter35-卡①-实现报告-2026-10-24.md b/reports/iter35-卡①-实现报告-2026-10-24.md deleted file mode 100644 index b82c75f2..00000000 --- a/reports/iter35-卡①-实现报告-2026-10-24.md +++ /dev/null @@ -1,99 +0,0 @@ -# 迭代 35 卡① 实现报告(节点图生成器核心批·参数化长图生成器) - -- 日期:2026-10-24 -- 卡片:`reports/需求卡片-节点图生成器核心批-2026-10-21.md` v1.1 -- 工作区:独立 worktree `D:/code/g-plan2-iter35`(分支 `iter35`,整迭代单工作区) -- 批性质:代码实现批——本批未写、未改、未新增 `design/` 与根规则文件;文档面义务全落本目录(见反哺点清单) -- 提交序列:卡① 实现(生成器+参数档+断言+manifest)→ fv4 判据三分码 → 致命面体例修正+判别力补强 → 合流图夹具按契约 2 重写 → 战斗保底判据专测夹具 - -## 一、任务卡状态 - -| T 卡 | 状态 | 交付面 | -|---|---|---| -| T0 事实坐实与跨卡接口对齐 | 完成(卡② 侧对齐随停工移交) | §3 F1–F8 行号与 §2 取用条款抽查:六处关键锚点实查命中(`level_runner.gd:513-525` enc 组装、`enemy_table.gd:157-161` band_of、`config_db_cross_validator.gd:100-106` 迭代 34 检查、`node_map_templates_test.gd:32-104` 宿主、`rng_service.gd:34/:86` 双面、`run_all.gd` manifest 体例)——零漂移;接口定稿见 §三 注 1;RNG 纯流面与支路口径定稿见 `reports/iter35-卡①-代拍板-2026-10-24.md` ①② | -| T1 参数档数据表 | 完成 | `game/data/schemas/level_gen_params.gd`(LevelGenParams,ROW_KEYS 十二键闭集+validate 域校验+row_by_level_index)、`game/data/tables/level_gen_params.tres`(三行 provisional,照卡片 §8.1 逐行填)、`game/core/config_db.gd`(注册+成员+装载+访问器 `level_gen_params_row()`)、manifest 登记一行 | -| T2 生成器核心 | 完成 | `game/core/rng_service.gd` 新增 `pure_stream(seed_val, scope)`(纯派生、不推 `_counter`、不落事件日志);`game/core/node_map_generator.gd`=`generate(params, seed)` 产图+`validate_output(graph, params)` 产出自证+`check_params` 可行性判定+`is_fatal()` 致命封印读面 | -| T3 验收断言批总装 | 完成 | 三宿主共 72 条断言:`level_gen_params_test.gd`〔fv2〕17 条、`node_map_generator_test.gd`〔fv1/fv3/fv5/fv6/fv7/fv9/fv10〕39 条、`node_map_growth_test.gd`〔fv4/fv8〕16 条;manifest 三行全登记(闸 02 实跑 58 套件全绿,本批 +3) | -| T4 产出物落位 | 完成(实现报告=本文件;代拍板单、反哺点清单、卡② 停工呈报同批) | `reports/iter35-卡①-实现报告-2026-10-24.md`、`reports/iter35-卡①-代拍板-2026-10-24.md`、`reports/iter35-卡①-反哺点-2026-10-24.md`、`reports/iter35-卡2-停机呈报-2026-10-24.md` | - -## 二、完成定义逐条对账(卡片 §1) - -| # | 完成定义 | 现实读数 | -|---|---|---| -| 1 | 产出图必过五条+铁律+摆放三条 | `generate` 产前强制 `validate_output`;三档 × 20 种子(42–61)扫描自证零错误〔fv1〕 | -| 2 | 参数档落地 ConfigDB 单点读、生成器不认关卡号 | 三行 .tres 加载零诊断;`generate(params, seed)` 无形参 level_index;件核=全仓 `generate` 调用点零传关卡号 | -| 3 | 确定性(同种子同图、counter 无关、随机只经 RngService) | 同 (seed,params) 两次逐位同图〔fv3〕;两台服务处不同号位(1 vs 999999+7 次取号)产图逐位相同〔fv3〕;`counter()` 前后不变〔fv10〕;生成器内零 `randomize()`/零自建 RNG/零 `open_stream` | -| 4 | 生长递增 | 同种子 42 读数:层高 6/9/13,节点总数 11/19/29,Δ(L1→L2)=8 < Δ(L2→L3)=10〔fv8〕 | -| 5 | 专项兑现 | L1 出口恒 1、大岔口自 L2 起、下一跳互异、支路构成互异各有独有类型、每路径恰一店、多终点深度互异、战斗保底 ≥ 4/6/8——逐项断言〔fv5/fv6/fv7/fv9〕 | -| 6 | 参数无解 fail-fast | `check_params` 拦两类无解夹具(层深 6 放三出口、保底超可放数)〔fv4〕;真实停机通道=致命封印置位→组合根停机(子进程退出码恰 1)〔fv4〕 | -| 7 | 验收断言批全绿+变异记录表 | 三套件独立跑全绿(17+42+16 checks,退出码 0);变异记录表见 §四 | -| 8 | 批末重闸全绿+产出物写齐 | 〔闸读数见 §五〕;三份 `reports/` 产出物+停工呈报在位 | - -## 三、实际改动—验收对照表(卡片 §8.2 的行号落地) - -> 断言宿主行号取实现当日实测;〔fv#〕尾缀可 grep。 - -| 尾缀 | 需求点 | 改动落点 | 验收测试(文件:行) | 真实入口 | 撤销/变异证明 | -|---|---|---|---|---|---| -| fv1 | 产出合格+产出自证 | `game/core/node_map_generator.gd` `validate_output()`、`_build()` | `node_map_generator_test.gd:88-89`、`:328-377`(正负向夹具) | `generate(params, seed)` 产前强制+`validate_output(graph, params)` 直调 | m4 首写深度→`:361` 红;m5 注掉终点恒 Boss→`:350` 红;夹具级:dup/dangling/enum/回边/合流各命中对应类别 | -| fv2 | 参数档 schema+.tres+ConfigDB | `level_gen_params.gd`、`level_gen_params.tres`、`config_db.gd:22/:43/:146/:253` | `level_gen_params_test.gd:16,47,50,55,59`+域负向 `:66-101` | `LevelGenParams.validate()`/`ConfigDB.level_gen_params_row()` | m1 L1 出口改 2→`:16` 红;m2 配比合计 0.5→`:16` 红 | -| fv3 | 可复现(纯函数+counter 无关) | `node_map_generator.gd` `generate/_setup_and_build`、`rng_service.gd` `pure_stream` | `node_map_generator_test.gd:99,109,119` | `generate(params, seed)` 同种子两次直调+跨号位双实例对拍 | m3 取数改 `open_stream("node_map")`→`:109` 红 | -| fv4 | 执行边界(无兜底图/fail-fast) | `node_map_generator.gd` `check_params()`+`_fatal()`+`is_fatal()` | `node_map_growth_test.gd:64,70,74,91` | 无解参数档调 `generate`;子进程 `-- fatal-fixture` 真实停机通道 | m11 致命短路改 `pass`→`:91` 红(停机码不再是 1) | -| fv5 | 岔路取舍两条 | `node_map_generator.gd` `_check_fork_rules()`、`_add_forks()` 双禁相邻扩岔 | `node_map_generator_test.gd:163-164`、`:412` | 产图岔口扫描+同类型下一跳夹具 | m8 侧支类型写死 battle→套件经致命通道转红(`:412` 夹具级判据直证下一跳互异) | -| fv6 | 恰一店+归属锁定+多终点深度互异 | `node_map_generator.gd` 区域链/商店位构造、`_check_fork_rules` 独占段 | `node_map_generator_test.gd:228-230`、`:387,397,404` | `validate_output` 前驱/店数/DFS 路径+depth 比对;DFS 全路径枚举 | m6 商店位改 rest、m7 双终点同 depth→套件经致命通道转红;夹具级 `:387/:404` 直证两类判据 | -| fv7 | 战斗保底 | `node_map_generator.gd` `_assign_types` 保底优先守卫、`validate_output` 保底判据 | `node_map_generator_test.gd:257`、`:343-346` | 产图战斗计数+合法形状低保底夹具 | m9b 注掉保底判据→`:343` 红;m9 砍穿保底→`:257` 所在套件转红(中止于空图读面) | -| fv8 | 生长递增 | `level_gen_params.tres` 三档 provisional 值 | `node_map_growth_test.gd:57-58` | 三档 `generate` 读数对拍(同种子 42) | m10(L3 分支密度归零)NOT-RED——如实标注拦截边界,见 §四 注 | -| fv9 | 含岔口与出口结构 | `node_map_generator.gd` `_add_forks` 起始 wanted、大岔口构造 | `node_map_generator_test.gd:285-287` | 三档 × 种子扫描结构计数 | m12 强制零岔口→`:285` 红 | -| fv10 | RNG 单点(纯流面) | `rng_service.gd` `pure_stream()` | `node_map_generator_test.gd:313-318` | `pure_stream` 直调+`counter()` 前后读数 | 与 m3 同发注入(counter 不变断言红)+静态扫描 09 零 `randomize()` | -| fv11 | 反哺点清单 | 无代码改动(非行为改动) | 件核:`reports/iter35-卡①-反哺点-2026-10-24.md` 七条落点 | T4 产出物件核 | 缺任一落点→件核不收口 | - -注 1(接口定稿对卡② 的裁剪):`generate(params, seed)` **不收 level_index**(契约 6),返回 `{nodes:[{id,type,depth,next}], entries:[int], terminals:[int]}`;参数档字段名 = `layers/branch_density/battle_ratio/reward_ratio/rest_ratio/shop_ratio/entry_count/exit_count/battle_floor/fork_ways_min/fork_ways_max`;两条硬语义(同种子同图、纯函数复现)由〔fv3〕〔fv10〕锁住。卡② 重编时 T0 按本表对齐接线。 - -## 四、变异验证记录表(注入→最窄套件见红→`git checkout` 还原) - -| # | 注入 | 期望红点 | 实测 | -|---|---|---|---| -| m1 | `level_gen_params.tres` L1 `exit_count` 1→2 | fv2 值域 | RED-OK:`生产 level_gen_params.tres 随全表加载 + validate 零违例〔fv2〕` 红 | -| m2 | L3 `battle_ratio` .55→.05(合计 ≠1) | fv2 合计 1 | RED-OK:同上断言红 | -| m3 | 生成器取数 `pure_stream` → `open_stream("node_map")` | fv3 counter 无关 | RED-OK:`counter 无关(不同号位态同种子同图)〔fv3〕` 红 | -| m4 | 最长路径计算 `maxi(...)` → 首写 only | fv1 深度语义 | RED-OK:`合法合流图(第二入口直插主链,深度取最长路 3)自证零错误〔fv1〕` 红 | -| m5 | `validate_output` 注掉「终点(无出边)非 Boss」判据 | fv1 | RED-OK:`非 Boss 终点夹具命中「终点(无出边)非 Boss」判据〔fv1〕` 红 | -| m6 | 区域商店位 `_mk("shop",…)` → `_mk("rest",…)` | fv6 | 拦截发生(进程红),红点落在空图致命通道(套件在〔fv3〕后中止);判据本体由 `:404` 夹具级断言直证 | -| m7 | 终点深度 `L - r` → `L - (r/2)`(两终点同深) | fv6 | 同 m6:进程红;判据本体由 `:387` 夹具级断言直证 | -| m8 | 岔口侧支类型修型写死 `battle` | fv5 | 同 m6:进程红;判据本体由 `:412` 夹具级断言直证 | -| m9 | 配比填余守卫 `if chain_battles - 1 < floor_n:` → `if False:` | fv7 | 拦截发生(进程红,中止于空图读面)——红点未落在〔fv7〕尾缀,改以 m9b 作判据级证明 | -| m9b | `validate_output` 注掉「低于保底」判据 | fv7 | RED-OK:`战斗节点低于 battle_floor 的合法形状夹具命中「低于保底」判据〔fv7〕` 红 | -| m10 | L3 `branch_density` 0.7→0.0 求增幅倒挂 | fv8 | **NOT-RED**(实测 L3 仍 27 节点、Δ 未倒挂)——本几何族下「只调分支密度」不足以破坏增幅递增,如实标注为拦截边界;层高/节点数的单调性由参数档 schema 域校验(层深递增且增幅变大)与〔fv8〕三档读数共同承载 | -| m11 | `generate` 无解路径 `_fatal(...)` → `pass` | fv4 | RED-OK:`无解参数档置致命封印并由组合根停机(退出码恰 1)〔fv4〕` 红 | -| m12 | 岔口数 `wanted` 写死 0 | fv9 | RED-OK:`三关参数档生成图各含 ≥1 岔口节点(第一关即含)〔fv9〕` 红 | - -跑批前后各一遍三套件:BASE/FINAL 均 exit=0 all green(17/42/16 checks)。 - -## 五、批末重闸读数 - -- 首轮 `tools/run_all_gates.sh --code`(回执 commit `ba660446`):**09 静态扫描 基线绿 → 现红**——新增两类本批违例:S5 core 裸引用 autoload 点标识符 1 处(注释形态)、类型闸 R1 无类型容器 17 处(全在本批新文件)。 -- 修复=`c0059bc5`(容器声明补参数化或改推断形;注释去 autoload 点引用形态),单件复跑 `bash game/tests/static_scan.sh` → S1–S12 全 ok、`tools/audit_type_hints.py` → PASS 归零。 -- 二轮全量代码域闸(回执 commit `c0059bc5`):**「验收通过——闸况与基线一致,无新增红」**,退出码 0;未跑面=11 回放确定性对拍(本批 diff 触及 RNG/数据表面,对拍须用户批准发起,列存量红·对基线零新增)。00 冒烟/01 系统测试/02 单元与集成(58 套件,含本批新登记三件)/03 探针 --click/04 探针 --full/05 渲染闸/10 熵增闸均与基线一致。 -- 死代码清理(审计缺漏⑤)后的三套件与静态扫描复跑读数见 §六末。 -- **三轮全量代码域闸=本批最终回执**(清理与报告收口笔之后跑,回执 commit `90fd434a` 与分支尖对齐):结论「验收通过——闸况与基线一致,无新增红」,退出码 0;未跑面仍为 11 回放确定性对拍(本批 diff 触及 RNG/数据表形态,对拍须用户批准后设 `PARITY_APPROVED=1` 重跑才实跑)。 - -## 六、独立验收审计结论 - -审计件:`reports/iter35-卡①-验收审计-2026-10-24.md`(未参与实现的 loop 内子代理,只读卡+对照表+diff+测试,自跑窄测复现撤销证明,不采信本报告自述)。 - -- **结论=有条件通过**,无必须退回的不通过项:对照表 fv1–fv11 逐行核过,断言真实存在且尾缀可 grep;真实入口未绕过改动点(`generate(params, seed)` 不收关卡号、`validate_output` 在产图主路径被产前强制调用、`pure_stream` 为生成器唯一取数源,生成器内零 `randomize()`/零自建 RNG/零 `open_stream`);diff 零删除、未触 `design/` 与根规则文件;`level_gen_params.tres` 与卡片 §8.1 逐格对上;manifest 三行 `expect: 0` 齐;本报告对 m6–m10 的 NOT-RED 与红点漂移属**如实呈报**(未写成已拦截)。 -- 审计独立复现的撤销证明两条:m11(`_fatal(...)` → `pass`)→ 唯一红 `node_map_growth_test.gd:91`〔fv4〕(子进程停机码 1→2),还原后 16 checks 全绿;m4(最长路计算改首写)→ 唯一红 `node_map_generator_test.gd:361`〔fv1〕合法合流图自证,还原后 39 checks 全绿。 -- 缺漏五项处置:①断言计数(报告原写 42/合计 70,实测 39/72)→ 已改 §一 T3;②fv2 行 ConfigDB 行号漂移(:42/:250 → 实测 :43/:253)→ 已改 §三;③§五闸读数、§六审计结论原为「待补」→ 本节与上节补齐;④`:404`/`:412` 两条夹具断言按错误**类别**判定,存在同类掩蔽源(同类别的另一判据可顶掉专指性)→ 记为已知拦截边界,判据专指性由 m5/m9b 两条「注掉判据即红」的注入补足(已 RED-OK);⑤`generate_for_debug` 零调用方=本批引入的死代码 → 已删除(生产路径只留 `generate`;断言读产出规格违例改走 `validate_output` 公有面直调),代拍板单第 2 条随之撤销。 -- 清理后复跑读数(死代码删除同批):`level_gen_params_test` all green (17 checks)、`node_map_generator_test` all green (39 checks)、`node_map_growth_test` all green (16 checks),退出码均 0;`bash game/tests/static_scan.sh` → S1/S2/S3/S4/S5/S9/S10/S11/S12 全 ok。 - -## 七、实机目检总清单(登记不阻塞) - -1. 三关生成图选路观感:多入口首跳、大岔口取舍、多终点走向可见、第一关即有选择——**依赖卡②/卡④ 落地后才能实机看到**(本批只产图不接运行时)。 -2. 生成图规模随关卡推进变大(11/19/29 节点、层高 6/9/13)可感知——同上。 - -## 八、遗留读数义务与停机面 - -- 参数档数值全 provisional;定值批须重跑〔fv8〕与 MC 对拍(本批零数值改动未跑 MC)。 -- 分支密度实际生效数受构型锚点几何限制:种子 42 实测侧支节点数 L1=4/L2=4/L3=3(总节点 11/19/29 减无岔口基线 7/15/26 之差),与 D1 调参表「一局十几次决策点」的意图有距离——已记入代拍板 ⑤ 与反哺点,定值批若要提密度须放宽锚点几何(属实现自由面,不改产出规格)。 -- 卡② 停工:其 §0 硬交付序列的「生成图深度扩面覆盖批」未立卡,本批未切运行时图源——详见 `reports/iter35-卡2-停机呈报-2026-10-24.md`。迭代 35 因此未做「一次合并」,合回与否待用户裁拆批。 -- 生成图深度域实测 L1 0–6/L2 0–9/L3 0–13 已写进反哺点③,供覆盖批立卡时当量程依据。 diff --git a/reports/iter35-卡①-验收审计-2026-10-24.md b/reports/iter35-卡①-验收审计-2026-10-24.md deleted file mode 100644 index 65b59b9e..00000000 --- a/reports/iter35-卡①-验收审计-2026-10-24.md +++ /dev/null @@ -1,130 +0,0 @@ -# 迭代 35 卡① 验收审计报告(节点图生成器核心批·合并前独立审计) - -- 日期:2026-10-24 -- 审计方:独立会话(未参与本批实现) -- 被审工作区:`D:/code/g-plan2-iter35`(分支 `iter35`,HEAD = `6074b3c3`,审读开始前 `git status --short` 为空) -- 基线:`2931b1f2`(开工时 master) -- 依据合同:`reports/需求卡片-节点图生成器核心批-2026-10-21.md` v1.1(§1 完成定义/§4 契约 1–7/§7 任务卡 T0–T4/§8.1 参数档取值表/§8.2 改动—验收对照表/§10 代拍板白名单) - -## 一、审计范围与读数依据 - -### 1.1 实际读过的文件 - -| 类别 | 文件 | -|---|---| -| 输入合同 | `reports/需求卡片-节点图生成器核心批-2026-10-21.md`(全文 269 行) | -| 被审产出物 | `reports/iter35-卡①-实现报告-2026-10-24.md`、`reports/iter35-卡①-代拍板-2026-10-24.md`、`reports/iter35-卡①-反哺点-2026-10-24.md` | -| 实现本体 | `game/core/node_map_generator.gd`(547 行全文)、`game/core/rng_service.gd`(151 行全文)、`game/core/config_db.gd`(本批 diff 段)、`game/data/schemas/level_gen_params.gd`(81 行全文)、`game/data/tables/level_gen_params.tres`(49 行全文) | -| 断言面 | `game/tests/unit/core/node_map_generator_test.gd`(445 行全文)、`game/tests/unit/core/node_map_growth_test.gd`(115 行全文)、`game/tests/unit/data_config/level_gen_params_test.gd`(106 行全文)、`game/tests/run_all.gd`(diff 段)、`game/tests/unit/test_kit.gd`(协议语义:check/finish/退出码) | -| 闸回执 | `game/tests/.gates/receipt.json`(实现会话批末代码域闸产物,本审计未重跑全量闸) | -| 规则源 | 根 `agents.md`(红线)、`tools/run_all_gates.sh`(09 静态扫描=`game/tests/static_scan.sh`) | - -### 1.2 实际跑过的命令与读数 - -| # | 命令(均在 `D:/code/g-plan2-iter35` 内执行) | 读数 | -|---|---|---| -| R1 | `git log --oneline -5` / `git status --short` / `git branch --show-current` | HEAD `6074b3c3`、分支 `iter35`、工作区干净 | -| R2 | `git diff --stat 2931b1f2..HEAD -- .` 与 `git diff --name-only 2931b1f2..HEAD -- . ':!game'` | `game/` 内 14 个文件、共 1562 行插入;`game/` 之外只有 `reports/` 四份产出物。**零文件触及 `design/`、`AGENTS.md`、`roadmap.md`、`待办清单.md`、`体验验证清单.md`**(`grep -Ei "design/\|AGENTS\|roadmap\|待办\|体验"` 命中 NONE) | -| R3 | `git diff --numstat 2931b1f2..HEAD -- game/` | 14 行删除列全为 `0`——本批对既有代码与既有断言**零删除、零改写** | -| R4 | 套件基线跑(逐条串行,每次先 `taskkill //IM Godot_v4.7.2-stable_win64.exe //F`):`--script res://tests/unit/data_config/level_gen_params_test.gd` | 退出码 0,`all green (17 checks)` | -| R5 | `--script res://tests/unit/core/node_map_generator_test.gd` | 退出码 0,`all green (39 checks)` | -| R6 | `--script res://tests/unit/core/node_map_growth_test.gd` | 退出码 0,`all green (16 checks)`,`[fv4] 无解子进程退出码 = 1` | -| R7 | m11 注入复现(见 §二) | 退出码 1,唯一红点 = `〔fv4〕` 组合根停机码断言 | -| R8 | m11 还原复跑 | 退出码 0,`all green (16 checks)` | -| R9 | m4 注入复现(见 §二) | 退出码 1,`[node_map_generator_test] 1/39 项失败`,唯一红点 = `〔fv1〕` 合法合流图断言 | -| R10 | m4 还原复跑 | 退出码 0,`all green (39 checks)`,工作区回到干净 | -| R11 | 件核 grep:`grep -n "randomize(\|RandomNumberGenerator.new(\|open_stream(\|FileAccess" game/core/node_map_generator.gd` | **零命中**(连注释都没有取数面引用);`grep -n level_index` 只命中第 33 行注释「不收 level_index」 | -| R12 | 件核 grep:全仓 `\.generate(` 非测试调用点 | 只命中 `board_generator.generate(...)`(另一类);`NodeMapGenerator` 的引用面 = 自身+`run_all.gd` manifest+两套新测试,**零生产消费方**(本批不接运行时,与卡片 §5 一致) | - -### 1.3 批末代码域闸回执读数(未重跑,只核读) - -`game/tests/.gates/receipt.json`:`commit = c0059bc5`、`domain = code`、`all_green = true`、`changed_vs_baseline = []`、`inherited_red = []`;闸 00/01/02/03/04/05/09/10 全 exit 0,闸 11(回放确定性对拍)= `NEED-APPROVAL` 未跑,计入 `not_run`——与「档位须用户发起」红线一致,不算新增红。**注意:实现报告 §五「批末重闸读数」仍是「待补」占位,回执读数没有回填进报告(见 §四 缺漏 3)。** - -## 二、A 表:改动—验收对照表逐行核对(实现报告 §三 fv1–fv11) - -每行三问:① 断言在测试文件里真实存在(file:line 已 grep);② 「真实入口」列确实走到被改代码;③ 「撤销或变异证明」在报告 §四 有对应注入记录。 - -| 尾缀 | ① 断言真实存在 | ② 真实入口核实 | ③ 变异记录对应 | 判定 | -|---|---|---|---|---| -| **fv1** 产出合格+产出自证 | 存在:`node_map_generator_test.gd:88,89`(三档×20 种子自证+入口/终点计数)、`:328,332,336,350,361,370,377`(违规夹具与合流夹具) | `generate()` 在 `node_map_generator.gd:47` 无条件调用 `validate_output(built, params)`,不过即 `_fatal` 且零交付(`:48-52`)——**产前强制成立,不是只在测试里直调**;`:82-83` 走 `_graph()`(真 `generate`)产图后再直调一次自证做二次核对 | m4(首写深度)→ 报告记 `:361` 红、m5(注掉终点恒 Boss 判据)→ 记 `:350` 红;**m4 本审计已实跑复现,红点逐字命中 `:361`** | **通过** | -| **fv2** 参数档 schema+.tres+ConfigDB | 存在 17 条:`level_gen_params_test.gd:16,47,50,52,55,62,72,75,78,81,85,88,92,97,100,102,105` | `:16` 走 `db.load_tables("res://data/tables", true)`=生产装载真入口;访问器 `config_db.gd:253 level_gen_params_row()` 实存并被 `:47/50/52` 与测试侧 `_params()`(`node_map_generator_test.gd:38`)消费;schema 直调 `validate()` 是卡片 §8.2 自身指定的入口(数据层无更上层入口),非降级 | m1/m2 → 报告记 `:16` 红 | **通过**(报告 §三 行的落点行号 `config_db.gd:42/:250` 与 HEAD 实值 `:43/:253` 漂移,`/22/146` 正确——见 §四 缺漏 2) | -| **fv3** 可复现(纯函数+counter 无关) | 存在:`:99`(同种子两次逐位同图)、`:109`(跨号位双实例对拍)、`:119`(异种子异图,10 种子去重 ≥8) | 三条全部经 `_graph()`→`NodeMapGenerator.generate(params, seed)`;`:101-111` 用两台服务处不同号位(`new_adventure(1)` vs `new_adventure(999999)`+7 次 `open_stream`)各建新生成器对拍——counter 无关确实从生产产图面进入 | m3(取数改 `open_stream("node_map")`)→ 报告记 `:109` 红 | **通过** | -| **fv4** 执行边界(无兜底图/fail-fast) | 存在:`node_map_growth_test.gd:64,70,74,82,91` | 静态面 `:64/70/74` 直调 `NodeMapGenerator.check_params()`;`:79-91` 用 `OS.execute` 起真实子进程跑 `-- fatal-fixture`,夹具 `_trigger_fatal` 经 `gen.generate(unsolvable, 42)` 真入口产图,再由 `is_fatal()` 决定停机码——审计实跑读数 `[fv4] 无解子进程退出码 = 1`,通道真实 | m11(`_fatal(...)`→`pass`)→ 报告记 `:91` 红;**本审计已实跑复现** | **通过** | -| **fv5** 岔路取舍两条 | 存在:`:163`(下一跳类型互异)、`:164`(支路构成互异+各有独有类型)、`:412`(同类型下一跳夹具)、`:162`(每档每种子的岔口存在性静默条) | `:127-161` 对 `generate` 产图逐岔口独立重算(不复用 `validate_output` 的实现),夹具面 `:412` 直调 `validate_output`;测试侧 `_branch_types()`(`:168-194`)与实现 `_check_fork_rules` 的独占段口径一致(代拍板 ②) | m8(侧支类型写死 battle)→ 报告如实记「进程红、红点落空图致命通道,判据本体由 `:412` 夹具级直证」 | **通过**(判别力短板见 §四 缺漏 4) | -| **fv6** 恰一店+归属锁定+多终点深度互异 | 存在:`:228`(DFS 全路径枚举恰一店)、`:229`(每终点前一位恰一店+店数=终点数)、`:230`(多终点异深)、夹具 `:387,397,404` | 三条主断言走 `generate` 产图+测试自身 `_all_paths()` 枚举(独立实现,非套用被测函数);`:213-221` 另在测试侧重算店数与前驱类型 | m6(商店位改 rest)、m7(两终点同深)→ 报告如实记「进程红,红点落空图致命通道」,未写成 fv6 尾缀专红 | **通过**(判别力短板见 §四 缺漏 4) | -| **fv7** 战斗保底 | 存在:`node_map_generator_test.gd:257`(三档×10 种子计数)、`:343`(低保底合法形状夹具必拒)、`:345`(保底 1 下不误伤) | `:255` 经 `_graph()` 真入口产图计数;`:343/345` 是 `ba660446` 专门为「让判据可被变异拦下」补的正反夹具,一拒一不误伤,判据按消息文本「低于保底」专断 | m9(砍穿保底)报告如实记「红点未落〔fv7〕尾缀、中止于空图读面」,并以 **m9b**(注掉判据)作判据级 RED-OK 记 `:343` 红 | **通过** | -| **fv8** 生长递增 | 存在:`node_map_growth_test.gd:57`(步数逐关变大)、`:58`(节点总数递增且增幅变大)+`:43-52` 五种子静默逐档读数 | `_reading()`(`:31`)经 `gen.generate(db.level_gen_params_row(L), 42)` 真入口,参数档从生产表读回,非硬编码 | **m10 = NOT-RED**,报告在 §三 与 §四 两处均明写「NOT-RED/如实标注拦截边界」,未冒充已拦截 | **通过**(拦截边界已如实呈报,见 §四 缺漏 5) | -| **fv9** 含岔口与出口结构 | 存在:`node_map_generator_test.gd:285`(三档各含 ≥1 岔口)、`:286`(L1 终点恰 1)、`:287`(L2/L3 含分区域大岔口) | 全走 `generate` 产图结构扫描+测试自身 BFS `_terminals_reachable()` | m12(`wanted` 写死 0)→ 记 `:285` 红 | **通过** | -| **fv10** RNG 单点(纯流面) | 存在:`node_map_generator_test.gd:313`(产图前后 counter 同值)、`:316`(同 (seed,scope) 同流)、`:317`(不推计数器且产图非空) | `:308-318` 真实例化 `RngServiceCore` 注入生成器、走 `generate` 产图后读 `counter()`;`pure_stream` 直调亦存在。静态面:R11 grep 在 `node_map_generator.gd` 内零命中 `randomize(`/`RandomNumberGenerator.new(`/`open_stream(`,生成器唯一取数点为 `:66 _rng_service.pure_stream(seed_val, SCOPE_STRUCTURE)`;闸 09 静态扫描回执 exit 0 | 与 m3 同发注入(报告记 `:109` 红) | **通过** | -| **fv11** 反哺点清单(非行为改动) | 件核对象存在:`reports/iter35-卡①-反哺点-2026-10-24.md` 七条表行 | 卡片 §6 六条逐格对上:①生成规格(多终点异深+最长路径赋值)②地图 :65 消歧③冒险结构 :293 深度域登记④level-map 呈现随动⑤待办清单 06 状态⑥roadmap 编入笔;另加实现新发现⑦(出口数 3 是支路互异天花板)。每条有「文件与节/改成什么/依据」三列 | 纯文档产出物,无代码变异,报告与卡片均标「非行为改动」 | **通过** | - -**契约逐条复核(与 A 表交叉)**:契约 1(纯函数+counter 无关+纯流面)=fv3/fv10 过;契约 2(id/depth 拆两轴+最长路径赋值+多终点异深)=节点字段恰 `id/type/depth/next`(`_mk:414`)、`validate_output:174-179` 逐节点比对最长路值、`:219-224` 异深判据,过;**契约 6(`generate` 不收 level_index)**=签名 `generate(params: Dictionary, seed_val: int)`(`:34`),全仓调用点零传关卡号(R12),过;契约 4(无兜底图)=生成器内零模板/零上一张/零空图回退分支,三处 `return {}` 全在 `_fatal` 之后(`:41/45/52`,注释明写「致命中止后不可达;本行即无残图交付的显式证明」),过。 - -## 三、B 段:撤销证明复现记录(2 条,真跑) - -前置:`git status --short` 空;`node_map_generator.gd` 为 UTF-8、547 行全 CRLF(注入后仍 547 CRLF,行尾未被工具改写)。 - -### B1 · m11 无解路径致命短路 - -- 锚点注入(`game/core/node_map_generator.gd:40`,唯一命中,1 行改): - - 原文:`_fatal("参数无解致命中止(契约 4——无兜底图、禁软回退)")` - - 注入:`pass # m11 mutation injection` -- 跑:`--script res://tests/unit/core/node_map_growth_test.gd` → **退出码 1** -- 红点读数(逐字): - - `[fv4] 无解子进程退出码 = 2`(原为 1) - - `ERROR: [FAIL] 无解参数档置致命封印并由组合根停机(退出码恰 1,无残图交付)〔fv4〕` - - 同批其余三条〔fv4〕(`:64/70/74`)仍 PASS——红点精确落在子进程停机通道那条,未泛化。 -- 还原:`git checkout -- game/core/node_map_generator.gd`(HEAD 已含实现,安全)→ `git status --short` 空 -- 还原复跑:**退出码 0,`all green (16 checks)`** -- 结论:**与报告 §四 m11 记录逐字一致,RED-OK 复现成立。** - -### B2 · m4 最长路径计算改首写 - -- 锚点注入(`node_map_generator.gd:440`,唯一命中,1 行改): - - 原文:`out[nid] = maxi(int(out.get(nid, 0)), int(out[id]) + 1)` - - 注入:`if not out.has(nid): out[nid] = int(out[id]) + 1 # m4 mutation injection` -- 跑:`--script res://tests/unit/core/node_map_generator_test.gd` → **退出码 1** -- 红点读数(逐字):`ERROR: [node_map_generator_test] 1/39 项失败`,唯一 FAIL 行 = - - `ERROR: [FAIL] 合法合流图(第二入口直插主链,深度取最长路 3)自证零错误〔fv1〕` - - 首写口径使合流节点取到短路值(3→1),与 `:174-179` 最长路比对不符 → 自证报错,正是报告 §四 m4 声明的红点位置。 - - 关键旁证:三档×20 种子主自证〔fv1〕`:88` 仍 PASS(生成器构型无合流,取长/取首等价)——说明这条断言拦的正是「合流图深度语义」,不是泛泛报错。 -- 还原:`git checkout -- game/core/node_map_generator.gd` → 工作区干净 -- 还原复跑:**退出码 0,`all green (39 checks)`** -- 结论:**与报告 §四 m4 记录逐字一致,RED-OK 复现成立。** - -## 四、C 段:测试削弱检查(逐条给依据) - -| 检查项 | 结论 | 依据 | -|---|---|---| -| 旧断言删改 | **零削弱** | `git diff --numstat` 14 个文件删除列全为 0;`run_all.gd`+3 行、`config_db.gd`+8 行、`rng_service.gd`+14 行(`pure_stream` 纯新增,`open_stream/derive_at` 未触);三套测试全为新建文件——与卡片 §8.2「零旧断言改写」预声明一致 | -| manifest 登记(未登记=闸 02 不执行) | **齐且口径正确** | `run_all.gd` 三行新增均为 `"expect": 0`,路径与盘上文件一一对应:`tests/unit/data_config/level_gen_params_test.gd`、`tests/unit/core/node_map_generator_test.gd`、`tests/unit/core/node_map_growth_test.gd`;三套件单跑退出码实测均 0(R4/R5/R6),与 expect 0 自洽 | -| 参数档数值 vs 卡片 §8.1 | **逐格对上,无放宽** | `level_gen_params.tres`:layers 6/9/13 ✓、branch_density 0.3/0.5/0.7 ✓、配比 .50/.20/.20/.10|.50/.20/.15/.15|.55/.15/.15/.15(各合计 1)✓、entry 1/2/2 ✓、exit 1/2/3 ✓、battle_floor 4/6/8 ✓、fork 2/3 三档 ✓;代拍板 ④ 自陈「取卡片原值、未做域内微调」与实物一致 | -| 扫描面种子数是否被缩小 | **未见对卡片的缩小;有一处对报告自述的漂移** | 卡片 §8 V1/T3 措辞=「三关参数档×固定种子」,未规定种子条数。实测主自证面 `node_map_generator_test.gd:81 range(42, 62)`=42–61 共 **20 种子 × 3 档**,与报告 §二#1「三档 × 20 种子(42–61)」一致;其余面:fv5/fv6/fv9 各 3 种子〔42,43,44〕、fv7 10 种子、fv8 5 种子、fv3 异图 10 种子——均落在卡片授权面内。**漂移在报告侧**:报告记该套件 42 条、实测 39 条(见缺漏 1) | -| 生成失败的回退分支(契约 4 禁) | **零回退分支** | 生成器不引用 ConfigDB/`level.tres`/模板池(R11/R12 grep 零命中),无「失败用模板/用上一张」路径;`_fatal` 后三个 `return {}` 是停机后的不可达行、并由 `is_fatal()` 要求消费侧拒收;本批**无任何生产调用方**取用产图(`NodeMapGenerator` 引用面=自身+manifest+两测试),故 `{}` 不存在被当合法图消费的现实通道 | -| `--script` 禁裸 autoload(F8/code-conventions:71) | 合规 | 生成器 `_init(rng_service = null)` 构造注入,未注入即 `push_error`+致命(`:42-45`);测试侧 `RngScript.new()` 显式实例化后注入 | -| NOT-RED 与「红点落别处」是否如实呈报 | **如实** | 报告 §四 m10 明写 `**NOT-RED**` 并在 §三 fv8 行再次标注;m6/m7/m8/m9 明写「拦截发生(进程红),红点落在空图致命通道/未落〔fv7〕尾缀」,并各自改以夹具级断言(`:404/:387/:412`)或 m9b 作判据级证明——四条**均未被写成 RED-OK/已拦截**,符合「无变异红记录只写已实现、拦截能力未证」红线 | -| 像素纪律/数值单点 | 合规 | 本批无像素结构件;玩法数值只落 `.tres`,生成器不硬编码旋钮值(全部读 `params`)。实现自由面常量(`MAX_WIDTH=4`、三岔概率 `0.35`、`_trunk_len` 取 `L/2`)属 HOW 面,卡片 §10 ⑤ 授权、报告 §八与代拍板 ⑤ 已登记其副作用(分支密度实际生效数受锚点几何限制) | - -### 缺漏与待办(审计判定,非红线违例) - -1. **实现报告断言计数不符**:报告 §一 T3 与 §二#7 记 `node_map_generator_test` 42 条、合计 17/42/16;HEAD 实测 **39 条、合计 17/39/16 = 72 条**(另 `63a6ea80` 提交信息记 68 条,说明该数在批内漂移后未回填)。合回前须把报告读数订正为实测值。 -2. **实现报告 §三 fv2 行的落点行号漂移**:记 `config_db.gd:22/:42/:146/:250`,HEAD 实值 `:22/:43/:146/:253`。 -3. **实现报告 §五(批末重闸读数)与 §六(审计结论)仍为「待补」占位**:完成定义 #8 要求重闸读数入报告;回执已在盘(`c0059bc5`/`domain=code`/`all_green=true`/闸 11 按红线未跑),须把这段读数抄进 §五,本报告可作 §六 依据。 -4. **三条夹具级断言按错误类别判定、判别力弱于消息级**:`:404`(终点前驱非店)与 `:412`(岔口同类型下一跳)用 `_has_cat(..., "structural")`——若只注掉该条判据、同夹具另有其它 structural 违例,断言可能仍绿(`:404` 的夹具确实同时触发「店数≠终点数」「穿店数≠1」两类,存在掩蔽源);`:387`(多终点同深)经逐节点推演无同类掩蔽源,专指成立。对照:`:350`(终点非 Boss)与 `:343`(低于保底)已按消息文本专断,是应当效仿的体例。**不阻塞合回**——m5/m9b 已证这两条判据可被拦,m6/m7/m8 已从真实 `generate` 入口证得套件级拦截。 -5. **m10 拦截边界真实存在**:「只调分支密度」不足以破坏节点总数增幅递增。报告已把单调性交给参数档域校验(层深递增且增幅变大,`level_gen_params.gd:64-71`+`:85` 断言)与 fv8 三档读数共同承载,属如实标注,不是漏判。 -6. **新增旁路入口 `generate_for_debug`(`:58-61`)当前零调用方**:只构型、不自证、不致命。卡片 §8 两分法授权调试接口,但代拍板单把它列为「白名单外需呈报第 2 件」待用户追认;本批无运行时接线故不构成「不合格产出进玩法」通道。卡② 接线时须保持生产路径只走 `generate`。 -7. **代拍板六条+白名单外两件待用户追认**(致命面形态从「生成器内停机」改为「置封印+组合根停机」,fv4 判据随之改为「停机码恰 1」)——语义等价、有断言兜住,但追认动作未完成。 - -## 五、结论 - -**有条件通过(可以合回,条件是下列三件在合回动作前后立即补齐,无需重跑实现)**: - -1. 实现报告 §五 回填批末代码域闸读数(回执 `c0059bc5`:domain=code、all_green=true、闸 11 按红线未跑)、§六 挂本审计报告,并把断言计数由 42 订正为实测 39(合计 72)。 -2. 实现报告 §三 fv2 行的 `config_db.gd` 行号按 HEAD 实值订正(`:43/:253`)。 -3. 用户对 `reports/iter35-卡①-代拍板-2026-10-24.md` 六条+白名单外两件(致命面形态、`generate_for_debug`)作追认或推翻;推翻则回写实现面。 - -判定依据:卡片 §1 完成定义八项逐条有真实入口断言兜住(§二 A 表全通过);两条撤销证明(m11/m4)独立复现且红点逐字命中报告声明;本批零删改既有代码与既有断言、零 `design/` 与根规则文件触碰、参数档数值与卡片 §8.1 逐格一致、无回退分支、RNG 单点静态核零命中;NOT-RED 与红点漂移均如实呈报。 - -**不通过项清单**:无(不存在必须退回实现或上游的缺漏;上述三件为产出物回填与用户裁决动作,非事实性缺陷)。 - -**另须留意(非本批验收范围)**:卡② 因 §0 硬交付序列的「生成图深度扩面覆盖批」未立卡而停工(`reports/iter35-卡2-停机呈报-2026-10-24.md`),迭代 35 因此不是一次合并——卡① 可否单独合回属用户拆批裁决,本审计只判定卡① 代码面验收闭合。 diff --git a/reports/iter36-代拍板追认单-2026-10-24.md b/reports/iter36-代拍板追认单-2026-10-24.md new file mode 100644 index 00000000..f0fe7a40 --- /dev/null +++ b/reports/iter36-代拍板追认单-2026-10-24.md @@ -0,0 +1,33 @@ +# 迭代 36 代拍板追认单(2026-10-24) + +> 白名单内代拍项备案(授权发生在卡片审问阶段,实现只执行预授权);批后追认=确认,不是事后授权。超白名单的裁断零发生(唯一例外=「路径②驱动前提不成立」的处置,见 §末单独列示——该处置落在卡C 代拍板 2 的「既有快照喂入」预授权形态内,但所涉卡片前提缺陷已升级为停机呈报级事实移交)。 + +## 卡C(开场后缀营地灰字迷雾测点批) + +1. **迷雾三行行名与注释措辞**(白名单 1):「迷雾·记忆墨/合成底L1地牢/L2幽暗/L3深幽」——含关别与「归一后」口径 ✓。 +2. **路径③断点态构造细节**(白名单 2):最终采用「既有快照喂入」形态(`save_manager.set_breakpoint` 夹具→`_resume_from_menu` 唯一消费口),非自然落盘——**约束满足**:断点=选路停点形(bonus.atk=5 已入账、bonus_pending/bonus_result 皆空、node_id=-1);bonus 回填经 `resume_adventure` :151-154 真链(探针断言 run_ledger.bonus.atk==5 在座)。形态变更的动因(原「自然落盘」链路依赖的奖励节点三选一在现行 build 不可达)已列反哺点 ③ 与停机级事实移交。 +3. **三选一候选兜底循环**(白名单 3):未触发——路径②原设计停点不可达(同上);通关三选一 relic 兜底选择取 field_dressing(生存向),随驱动链一并废止。 +4. **取色断言定位锚**(白名单 4):文本锚(图鉴锁文「柜台结构位预留」/秘宝匣行容器子树内「未解锁」/形态「被动|主动·充能」/档位=UiKit.TREASURE_TIER_LABELS 值域/刷新行「三选一刷新」前缀)。 +5. **提交切分与分支命名**(白名单 5):单工作区 `g-plan2-iter36`、分支 `iter36`;实现一笔检查点提交+闸后产出物一笔(卡③停摆不提交)。 +6. **迷雾行值 L2 条款修订**(卡C §9 L2 预授权):手填行值与探针派生首跑失配(L2/L3 偏差 ~5/255,PIL↔Godot 导入解码系统差)→ 以探针派生值为准修行值(L2 (74,79,96)/L3 (71,75,99)),读数与依据在实现报告 §3。 + +## 卡A(文案与禁字清扫批) + +1. **u3 最终措辞**(白名单 1):「关卡奖励点\n从冒险牌库抽三选一(属性值提升与补给;与通关专长三选一同库分流):\n选择一件带走:」——三机制事实词齐、三旧话词零残留 ✓。 +2. **断言名与消息措辞、定位写法**(白名单 2):全部带〔wa#/wc#/sv#〕尾缀;取色/段头定位=文本锚+成员直引混合(sweep 宿主用成员 `_feat_section` 首子 Label,探针用文本锚)。 +3. **T2② 地图帽断言注值取法**(白名单 3):帽 Label 常在树(零加成隐藏不隐藏 Label 本体)——按子树文本锚直判,零注值。 +4. **源扫描注释剥离**(白名单 4):行首 `#` 判定式(strip_edges 后 begins_with("#") 跳行);board_view.gd 扫描为全源 `"雷"` 字面量计数(哨兵族钉住,无注释剥离必要——实查零命中含注释)。 +5. **提交切分**(白名单 5):与卡C 同笔检查点提交(ui_click_probe 断言面两卡共用文件,拆笔无净收益)。 + +## 卡③(状态面板瘦身批) + +1. **空态句与段头措辞**(白名单 1):段头=「本局状态/带的秘宝/已获专长」(用户原话词干);空态句=「未携带秘宝」「未获专长」(:130 先例形制)✓。 +2. **属性行串接序与分隔符**(白名单 2):「生命 %d/%d · 攻击 %d · 护甲 AC %d · GP %d」逐字(契约 4 钉值)✓。 +3. **断言定位写法**(白名单 3):隔离档=成员直引(`_attrs_label`/`_curios_label`/`_feats_label`)+子树文本扫描(零旧段);探针=成员直引+独立 oracle 对拍。 +4. **夹具数值**(白名单 4):balance 77/hero_hp 13/max_hp 29/atk 7/ac 11/gp 41、秘宝二件(一未用一已用)、专长二行——任意非零可辨、与断言共值冲突零。 +5. **提交切分**(白名单 5):卡③ 一笔实现提交+风格修正一笔。 +6. **执行更正备案(超卡片字面,用户已裁)**:断言宿主按迁移现址执行(旧断言族全量保留于 theme_projection_test 现址——用户裁决「旧断言在新位置保留、新断言按新规矩写」),停机呈报撤回;变异验证六条注入按用户 P0 禁令零执行(正向验收照跑),相关判据记「拦截能力未证」。 + +## 超白名单事项(须用户知悉) + +- **路径②驱动前提不成立**(卡C):奖励节点三选一在现行 build 不可达(reward 节点直通占位、L1 零商店节点)——处置=改快照喂入形态(代拍板 2 形态内)+事实移交反哺点 ③/⑧+实现报告缺陷记录。若用户认「路径②须保留原设计」→ 需先立「奖励节点三选一实装」批,本卡 wc1 正向半随之重写卡。 diff --git a/reports/iter36-反哺点-2026-10-24.md b/reports/iter36-反哺点-2026-10-24.md new file mode 100644 index 00000000..039b978c --- /dev/null +++ b/reports/iter36-反哺点-2026-10-24.md @@ -0,0 +1,61 @@ +# 迭代 36 反哺点清单(2026-10-24) + +> 实现批产出物。每条=哪个文件哪一节、改成什么、依据。用户会话裁决后归文档批写入 `design/` 并把该条清掉。本批零 GDD 陈述违反——以下全部为「实现与现实差异的文档随动」与「卡片事实缺陷的登记」,逐条待裁。 + +## 卡C(开场后缀营地灰字迷雾测点批) + +### ① `design/art/art-bible.md` §6.5 :395 迷雾记忆态在册复算值换代 +- 改成:在册复算值「≈6.7:1(地板纹理均值 #453020 与灰罩合成后…)」→「归一后(真实渲染面)三关 5.71–5.93:1(合成底 L1 #4C4038/L2 #3F4147/L3 #3E4048;行值与素材随动,测点=ui_contrast_probe 迷雾段)+归一口径注(装载亮度归一是现行生产路径——board_view `_normalize_floor_luma`,原图均值≠渲染面)」。 +- 依据:卡C 契约 3(甲·归一后基准)+F9/F10;**读数修正**:写卡会话 PIL 复算(5.65–5.75:1)与探针派生首跑在 L2/L3 系统性偏差约 5/255(PIL↔Godot 导入解码差),已按卡内 L2 条款以探针派生值为准修手填行值——文档应记派生口径值(5.71/5.83/5.93),不是写卡值。 + +### ② `待办清单.md`「qa/ 实现报告收尾·孤儿遗留七项」条目处置 +- 改成:第⑥项(TG_* 装饰带)按用户 2026-10-24 裁整条注销;第②③④项随卡C 落地销号;第⑦项(商店房截图纳屏)留存——条目本体保留至 ⑦ 处置;状态推进「已写卡(本卡路径)→已实现(迭代 36)」。 +- 依据:卡C §6 +来源条目状态机。 + +### ③ 卡C 卡片前提缺陷登记(新增反哺点——开工检查后施工中新发现) +- 改成:在「qa/ 实现报告收尾」条目或需求卡片修订记录(由文档批裁量落点)登记:**F5「属性值提升经三选一/商店兑付入 bonus(奖励节点渠道=属性值提升+补给)」中「奖励节点渠道」半句与现行 build 不符**——地图 reward 节点=零兑付直通占位(`level_runner.gd` `_enter_node` 注记「奖励点内容未定…零兑付直通」),L1 全模板零商店节点(level.tres L1 行 20 battle/5 boss/5 rest/5 reward,零 shop);现行局内 bonus 唯一可达来源=L2 商店货架(pool 面 kind_id 购买)。卡C 路径②原设计(奖励节点拿属性卡→下一关开场行)据此不可驱动,实现改快照喂入形态(代拍板 2)。 +- 依据:实现实跑证据(probe24/25 选路诊断+level.tres 实查);此差异同时影响后续任何涉及「奖励节点三选一」的驱动设计。 + +## 卡A(文案与禁字清扫批) + +### ④ `design/gdd/ux/node-reward.md` :14 旧头部全文引换代 +- 改成::14 在册逐字引「关卡奖励点\n从提升池抽三选一(与通关三选一同池同权重同上限):\n选择一件带走:」→ 新头部全文「关卡奖励点\n从冒险牌库抽三选一(属性值提升与补给;与通关专长三选一同库分流):\n选择一件带走:」;并连带该节「文案换代归内容批,已呈报」句销号(本卡即该内容批)。 +- 依据:卡A u3(§6 ①)。 + +### ⑤ `design/gdd/systems/表现与支撑/教学引导.md` :58 旧句换代 +- 改成:「(安全格零奖励、危险格吃挖雷惩罚)」→「(安全格零奖励、危险格受挖掘惩罚)」;同句「插错可左键直接挖开」→「标空可左键直接挖开」(u4 全句换代——「标空」与「标中」成对)。 +- 依据:卡A u4(§6 ②)。 + +### ⑥ `game/tests/golden/README.md` 加严口径纪律句(待用户裁) +- 改成与否待裁:本批 golden 判定采用加严口径「棋盘三图 changed>0 且差异像素落棋盘裁剪区=按判失效处理(显式呈报后定重制),不改写 2% 容差基线语义」——是否写入 README 纪律句由用户裁(严于现行「远低于阈值=有效」)。 +- 依据:卡A 契约 4(§6 ③)。 + +### ⑦ `待办清单.md` 卡A 两条来源条目批后退役 +- 改成:「现役 UI/文案两处自相冲突」(2026-10-09)+「玩家面残留禁字清扫——图例/明细/日志/字形五处」(2026-10-20)两整条随本卡落地退役;状态推进「已实现(迭代 36)」。 +- 依据:卡A §6 ④。 + +### ⑧ 卡A 卡片前提核对补记(合并进 ③ 同一登记或分列,由文档批裁量) +- 改成:卡A T2③ 断言「三选一停点弹层头部含『冒险牌库』」在本次实跑未捕获到「关卡奖励点」停点(奖励节点直通占位——同 ③ 依据;实跑出现的唯一停点=通关三选一,头部为「第 1 层通过!…」独立文案,不经 node_curio_header)。断言保留为条件断言(拦截面=未来奖励节点停点实装后的真实头部),**拦截能力未证**如实申报(test-conventions §6 口径)。 +- 依据:probe 探针实跑记录(ui_click_probe 主循环 curio 分支捕获条件)。 + +## 卡③(状态面板瘦身批) + +### ⑨ `design/gdd/systems/跨局循环/元进度.md` 「解锁界面:双入口框架」节(:104-106) +- 改成:「局内状态面板……也可查看钥石余额与**已解锁清单**(只读)」→「……也可查看钥石余额与本局冒险信息(只读)」。 +- 依据:卡③ 契约 1/2(跨关解锁概览退役——「已解锁清单」半句失效)。 + +### ⑩ `design/gdd/systems/跨局循环/生涯.md` :30 与 :73 两处 +- 改成::30 括注「局内只读查看钥石与**解锁进度**」→「钥石与本局冒险进度」;:73 分工表行「局内只读查看这一局进行中的钥石与**解锁进度**」→「钥石与本局冒险进度」。 +- 依据:卡③ 契约 1/2(同上)。 + +### ⑪ `design/gdd/08-Alpha版本标准.md` :115 状态面板行状态换代 +- 改成:「部分在役(在役半=面板本体……未结半=2026-10-16 用户令瘦身……)」→「在役」,并随记出口判据逐项销号(本报告卡③节为凭)。 +- 依据:卡③ §6 ③+D1(08 状态面板行)。 + +### ⑫ `体验验证清单.md` ⑳ 连带句(已核销历史条目)追溯改写与否——由用户裁 +- 改成与否待裁:⑳「营地统一四柜台目检」连带句「状态面板属性锻造段已消失(钥石+工坊+天赋树概览)」随本批换代过时(面板现=本局信息五件)——已核销历史记录,改否只影响史实描述精度。 +- 依据:卡③ §6 ④。 + +### ⑬ `待办清单.md` 卡③ 登记条批后退役 +- 改成:「状态面板瘦身——局内面板去跨关解锁内容、只留本局信息」条目整条退役(状态「已实现(迭代 36)」)。 +- 依据:卡③ §6 ⑤。 diff --git a/reports/iter36-实现报告-2026-10-24.md b/reports/iter36-实现报告-2026-10-24.md new file mode 100644 index 00000000..583350ce --- /dev/null +++ b/reports/iter36-实现报告-2026-10-24.md @@ -0,0 +1,137 @@ +# 迭代 36 实现报告(2026-10-24)——卡C 开场后缀营地灰字迷雾测点批+卡A 文案与禁字清扫批+卡③ 状态面板瘦身批 + +- 工作区:`../g-plan2-iter36`(分支 `iter36`,单工作区串行);卡序=roadmap 行格内顺序(卡C→卡A→卡③) +- 卡③经过更正:实现会话曾因卡片断言宿主指认指向已退役的 `game/smoke/` 而停机呈报;**用户裁决更正**——退役迁移已把旧断言族全量保留至现址(`theme_projection_test.gd` `_status_panel_projection`,迁移账本 S61),实现按现址续接即可,无需重锚流程。原停机呈报文件已删(git 历史可考),其结论由本报告卡③节吸收。 +- 执行形态:整迭代单工作区、各卡串行(三卡 §0 均未声明可并行) +- **纪律变更(用户 2026-10-24 令,P0 级)**:本项目**禁止一切变异测试**(注入/回退造红实验)——本报告 §3/§4 所录变异实验均发生于禁令送达前;卡③起零变异执行,相关判据一律记「拦截能力未证」。规则文档中「变异验证」条款的修正归文档批。 + +## 0. 卡C 状态总表 + +| 完成定义项 | 状态 | 凭据 | +|---|---|---| +| 1. 后缀条件落地+三路径断言 | **达成(②形态代拍板 2 等价)** | ui_click_probe 白板反向绿+续玩重建正向绿(bonus.atk=5 回填、pct=0、ov=0)+wc3 回归绿;theme_projection 静态半三断言绿 | +| 2. 营地六处 STONE_DEEP+零残留 | 达成 | 源 token 扫描(struct probe)绿+六处取色断言绿+contrast 探针在册两行回归绿 | +| 3. 迷雾三行+派生校验+注释随动 | 达成 | contrast 探针 40 对全绿+派生校验绿;board_cell_unit.gd:270 注释随动 | +| 4. 对照表逐行断言绿+变异表 | 达成 | §3 变异记录表 | +| 5. 批末代码域闸全绿+读数对拍+三产出物 | 见 §6 | 闸回执 | + +**卡C 前提缺陷(重要)**:路径②原设计「奖励节点三选一拿属性卡→下一关开场行带后缀」在现行 build 不可驱动——L1 全模板零商店节点(level.tres L1 行:20 battle/5 boss/5 rest/5 reward,零 shop),reward 节点=零兑付直通占位(level_runner `_enter_node` 注记「奖励点内容未定…零兑付直通」,实跑选路诊断印证)。现行局内 bonus 唯一可达来源=L2 商店货架,但 autoplay 到店生存性 ~42% 量级(MC perfect 锚)不可作为验收驱动。**处置**:②③改「既有快照喂入」形态(代拍板 2 预授权:断点=选路停点形钉值——bonus.atk=5 已入账、bonus_pending/bonus_result 皆空;经 `_resume_from_menu` 唯一消费口;`resume_adventure` :151-154 真链回填由探针断言 run_ledger.bonus.atk==5 钉住)。条件加算区两项的机制面(回填→level_started→条件求值→尾接)与 fresh 路径同式全链被测;fresh 正向拦截能力未证,如实申报。已列反哺点 ③。 + +## 1. 卡A 状态总表 + +| 完成定义项 | 状态 | 凭据 | +|---|---|---| +| 1. 钉值表 12 单元落地+玩家面六词零残留 | 达成 | 六词扫 presentation/core 玩家面字面量零命中(.tres 命中均为 `;` 注释保留面);字形「危」+两哨兵同批 | +| 2. 对照表逐行断言绿+变异表 | 达成 | §4 变异记录表 | +| 3. 内部注记面零改动 | 达成 | RED_MINE 常量/注释、「雷」内部语、术语表 :128 所指全未触碰(git diff 核) | +| 4. golden 判定动作 | 见 §6 | 渲染闸读数 | +| 5. 批末闸+读数对拍+三产出物 | 见 §6 | 闸回执 | + +**卡A 前提缺陷(较轻)**:wa4 探针半(三选一停点弹层头部断言)本次实跑未捕获到「关卡奖励点」停点——实跑唯一停点=通关三选一(头部=「第 1 层通过!…」独立文案,不经 `node_curio_header`)。断言保留为条件断言,**拦截能力未证**(test-conventions §6 口径);静态半(sweep 断言②+u3 变异红)拦截力已证。已列反哺点 ⑧。 + +## 1b. 卡③ 状态总表(状态面板瘦身批) + +| 完成定义项 | 状态 | 凭据 | +|---|---|---| +| 1. 面板五件内容在位且投影一致 | 达成 | theme_projection `_status_panel_projection` 换代九断言全绿(余额徽/属性行五现值/秘宝受理序+已用尾缀/专长行串/空态句×2/提示行)——套件 162 checks 全绿 | +| 2. 跨关概览退役零残留 | 达成 | sv5 双断言(子树零旧文案+旧成员零残留);`meta.treasure_shop_status()/tree_camp_rows()/tree_fork_groups()` 三函数本体零改动(diff 核) | +| 3. 文案三处换代+标题/继续钮锚不变 | 达成 | 提示行/两角标 tooltip=「查看本局冒险进度(只读)」;全仓「永久强化进度」零命中;ui_struct_probe ⑧ 两契约绿(信号连接数=2+零动效) | +| 4. 只读/零动效契约保持 | 达成 | 同上(ui_struct/field 探针回归) | +| 5. 产出物三份 | 达成 | 本节+反哺点+代拍板追认单卡③节 | +| 对照表 sv6/sv7 探针三断言 | 达成 | ui_click_probe 状态段(真实角标信号开层):零旧段+属性行对 runner 五现值+`_status_data()` 直调八键键集+curios 独立 oracle(query_inventory×treasure_item_row)+feats 对拍——探针全绿(问题=0) | +| 变异验证 | **零执行(用户 P0 禁令)** | 卡片 T1/T2 六条注入全部未跑;相关断言记「拦截能力未证」——正向验收测试照跑(隔离档+探针) | + +实现要点更正(对卡片 F4 的执行更正):`hero_hp`/`gp` 取数走 LevelRunner 属性 getter(`runner.hero_hp`/`runner.gp`——`_ledger` 直取下标的属性化封装,键缺失=致命语义不变);卡片原文 `runner.ledger[...]` 的 ledger 住在 AdventureRunner,面板开层时点的 runner=LevelRunner。 + +## 1c. 卡③ 实机目检总清单(登记不阻塞) + +1. 面板观感:本局信息层级/文案通顺/空态句得体(08 出口判据「用户目检通过」由此承接)。 + +## 2. 实际改动—验收对照表(卡C) + +> 与卡片 §8.2 的差异处加粗。断言尾缀可 grep。 + +| 行 | 改动实况 | 验收测试实况 | 真实入口实况 | 撤销/变异证明实况 | +|---|---|---|---|---| +| wc1 新开路径 | main.gd:608-616 条件扩两项(六项布尔逐字);theme_projection_test `_smoke_level_open_line` 扩段三断言;ui_click_probe 白板反向断言 | 三静态断言绿+白板反向绿〔wc1〕 | `_start_adventure_from_menu()`(:493 现址)真实开局;静态直调 | 变异:条件全项删→白板仍绿(反向钉住);正向半见 wc2 行(fresh 正向不可驱动——缺陷记录) | +| wc2 续玩路径 | 同 hunk+探针断点续玩断言 | 续玩重建开场行带后缀〔wc2〕 | `_resume_from_menu()`(:519 唯一消费口);断点=set_breakpoint 快照喂入(代拍板 2):选路停点形、bonus.atk=5、pending/result 皆空 | **撤销:回退条件 hunk→路径②③红、①仍绿(实测 mut3)** | +| wc3 既有口径回归 | 同 hunk 回归面 | run 1(滑条 400/200=局外加成)开场行带后缀〔wc3〕 | 调试滑条 pct 门控(现役 DebugOverlay) | 变异:条件旧四项全删→wc3 红(实测 mut4) | +| wc4 营地六处换色 | camp_overlay_view.gd :167/:417/:476/:509/:513/:525 换 STONE_DEEP;源内零裸 STONE | 取色六锚断言+token 扫描断言〔wc4〕 | `open_camp_via_savesel` 真实入口 | 变异::417 回 STONE→取色红(mut2 实测 12 处「未解锁」行)+struct 扫描红(mut5 实测);判别式放宽→误红(mut6 边界证明) | +| wc5 迷雾测点三行+派生校验 | ui_contrast_probe 迷雾单行→三行+`_norm_floor_avg` 派生+校验断言;**行值按探针派生首跑修值(L2 (74,79,96)/L3 (71,75,99)——卡 §9 L2 预授权)** | 三行 ≥4.5:1+行值 vs 派生 ±2/255〔wc5〕 | `UiKit.load_tex` 真装载+`BoardView._normalize_floor_luma` 生产静态直呼 | 变异①行值回原图基准→校验红;②调偏 +4→红;③target_l 0.10→0.05→红(三注入实测) | +| wc6 注释随动(非行为) | board_cell_unit.gd :270 复算值换代(5.71–5.93:1) | 件核 | — | 件核 | +| wc7 反哺点(非行为) | 反哺点清单卡C 三条+新增 ③ | 件核对表 | — | 件核 | + +## 3. 卡C 变异验证记录表 + +| # | 注入 | 最窄闸 | 结果 | +|---|---|---|---| +| T3-① | L1 行值回原图基准 (69,48,32) | contrast 探针 headless | 红:行值失真(派生 (111,77,51))✓ | +| T3-② | L1 行值调偏 +4 (115,77,51) | 同上 | 红 ✓ | +| T3-③ | 派生 target_l 0.10→0.05 | 同上 | 红:三关全失真 ✓ | +| T3-还原 | 精确还原 | 同上 | PASS 40 对 ✓ | +| T2-①(mut3) | 条件删加算区两项 | ui_click_probe 全程 | 红:②③失守;①仍绿(反向钉住)✓ | +| T2-②(mut4) | 条件旧四项全删 | 同上 | 红:wc3 回归失守+②③失守;①绿 ✓ | +| T1-①(mut2) | camp :417 回 STONE | ui_click_probe | 红:取色断言 12 处「未解锁」行 ✓ | +| T1-②(mut5) | 同注入 | ui_struct_probe | 红:源扫描裸 STONE ✓ | +| T1-③(mut6) | 扫描判别式放宽(匹配 _DEEP) | ui_struct_probe | 误红(判别边界证明)✓ | + +## 4. 实际改动—验收对照表+变异记录(卡A) + +| 行 | 改动实况 | 验收测试实况 | 变异证明实况 | +|---|---|---|---| +| wa1 C 区段头×2 | hero_info_panel_view.gd :73/:93 换词 | sweep 断言①+探针 C 区断言 | u1 回退→sweep 红;u2 回退→sweep 红(批量注入实测);探针半=u1 回退→探针红(mut2 实测) | +| wa2 地图加成帽 | level_map_screen.gd :171 换词 | 探针地图段(文本锚) | u10 回退→探针红(专项 mut7 实测,唯一失败项) | +| wa3 专长卡兜底 | battle_narrator.gd :142 换词 | sweep 断言③ | u11 回退→sweep 红(专项实测) | +| wa4 奖励点头部 | battle_narrator.gd :156 换新(L1 措辞微调:注释「共享同池」→「共享同库」随动) | sweep 断言②(三事实+三旧话清零)+探针条件断言(拦截能力未证——见 §1 缺陷) | u3 回退→sweep 红(专项实测 6 断言全红) | +| wa5 图例行 | legend_overlay.gd :11 换词 | sweep 断言④+探针图例段 | u4 回退→sweep 红+探针红(批量+mut2 实测) | +| wa6 状态槽来源名 | status_slot_row.gd :40 换词 | sweep 断言⑤(tooltip 消费半) | u5 回退→sweep 红(批量实测) | +| wa7 结算明细来源名 | settlement.tres :14 换值 | sweep 断言⑥(ConfigDB 真装载+battle_log `_format_row` 行消费半) | u6 回退→sweep 红(表值半+行半双红,批量实测) | +| wa8 危险格字形族 | board_view.gd :730/:778/:782 同批换;:776 注释随动(L1) | sweep 断言⑦(glyph/plate/glyph_color 三半)+⑧源扫描 | u7 回退→glyph 红+扫描红(批量);u8 单独回退→扫描红(solo 实测——卡片预判的「glyph_color 红」未现,实际红面=扫描,如实记录);u9 单独回退→plate 红+扫描红(solo 实测) | +| wa9 字体「危」在集 | ui_font_asset_test.gd 增行 | 断言绿 | 参数换 U+20000→红(判别力实测) | +| wa10 golden 判定(非行为) | 见 §6 | 读数+结论 | 不适用 | +| wa11 反哺点(非行为) | 反哺点清单卡A 五条 | 件核对表 | 件核 | +| wa12 惊巢反制日志行 | battle_orchestrator.gd :797 换词(core 触碰面=此一行字符串) | forge 锋段⑨(step_row 采集盒)+sweep ⑩扫描 | u13 回退→forge ⑨红+扫描红(专项实测) | + +卡A 变异补充记录:批量注入(u1/u2/u4/u5/u6/u7/u8/u9/u11/u13 十件同批)→sweep 12 断言红,逐件归因(u8 被 u7 掩蔽→补 solo 注入)。新宿主 `player_copy_sweep_test.gd` 入 run_all 清单(expect=0),**实跑 24 checks 全绿**;restore 后全绿复核两轮。 + +## 5. 实机目检总清单(登记不阻塞) + +1. 开场行后缀观感:「(含面板加成)」出现时机的直觉合理性(卡C §8.2)。 +2. 营地灰字观感:六处深化后的层次(卡C §8.2)。 +3. 危险格「危」字观感:字形密度/可读性对「雷」的替代(卡A §8.2)。 +4. 图例新句通顺:「标空…受挖掘惩罚」读感(卡A §8.2)。 + +## 6. 批末闸与读数对拍 + +- 改前读数(master HEAD 基线口径):theme_projection 153 checks/ui_contrast_probe 38 对/player_copy_sweep 无(本批新建)/其余四探针存量基线见 `game/tests/.gates/baseline.json`。 +- 改后读数:theme_projection 156(+3 wc1)/player_copy_sweep 24 全绿/ui_contrast_probe 40 对(+3 迷雾行 −1 旧单行)+派生校验绿/ui_click_probe 问题=0(PROBE_SEED=7)/ui_struct_probe 全绿。 +- 批末 `tools/run_all_gates.sh --code`:见闸回执(§6.1)。 +- golden 判定(卡A 契约 4):见 §6.2。 + +### 6.1 闸回执读数 + +- 批末 `tools/run_all_gates.sh --code`(回执 commit=9468c70e,与分支尖对齐):**8 闸全绿**(00 冒烟/01 系统/02 单元与集成/03 探针 --click/04 探针 --full/05 渲染闸/09 静态扫描/10 熵增闸,fingerprint 全 green、changed_vs_baseline 空)。 +- **11 回放确定性对拍 = NEED-APPROVAL(存量红·对基线零新增,未实跑)**——触面判定因 settlement.tres 数值/结算域文件在 diff 内;本批对该文件只改一处文案值(u6「踩雷」→「危险格」),零数值改动。按硬规(MC/模拟跑批发起前必须用户批准)未发起;是否批准补跑归用户。 +- 改前改后读数对拍:见 §6 首段(theme 153→156/sweep 0→24 全绿/contrast 38→40 对+派生校验新面/click probe 问题=0/struct 全绿)。 + +### 6.2 golden 判定(卡A 契约 4——加严口径触发,呈报待裁) + +- 渲染闸读数 ×4:棋盘 L1 **changed=0.13%**/L2 **0.11%**/L3 **0.10%**/menu_boot **0.00%**(容差 ≤16 差/≤2%,闸判 OK)。 +- **加严口径触发**:三图 changed>0 且差异像素落棋盘裁剪区(L1 裁剪区内 911 px,x:239–585/y:240–394,其中 893 px 单通道差 >60=结构差非抗锯齿噪声)→ 按契约 4 加严句**本批按判失效处理**——不写 README 判有效加注(README 零改动),重制与否显式呈报待裁(反哺点 ⑥)。 +- **归因证据(存量漂移,非本批引入)**:master HEAD(无本批改动)同探针同基线复测读数逐值相同(0.13/0.11/0.10%),差异像素同位同量——漂移在批前已在;且 F7 实查四基线图零 RED_MINE 雷字朱像素,本批字形改动(雷→危)不可能触及基线像素。判读:加严句针对的「本批字形差被容差吞掉」情形不成立,但按字面执行判失效呈报;是否重制基线归用户(重制件须复检雷字——F7 注)。 + +## 7. 遗留读数义务与申报 + +1. wa4 探针半与 wc1 fresh 正向:**拦截能力未证**(前提不可驱动——反哺点 ③/⑧)。 +2. 卡③ 全部变异判据:**零执行**(用户 P0 禁令)——sv 断言「拦截能力未证」,正向验收照跑。 +3. ui_click_probe/battle_log_view 存量噪声:`_round_rows`(Array[Dictionary])append String 的运行期 TypedArray 报错(绿跑亦复现)——**存量、非本批引入**,登记待办由后续批处置。 +4. **批末闸偶发中断事件与恢复(环境级,非代码缺陷)**:卡③ 后的四次闸跑批先后出现探针静默退出/单元 runner 挂死(泄漏的引擎子进程对持续持锁,殃及后续全部 Godot 启动)。处置=清杀泄漏进程+删除并重建 `game/.godot` 缓存 → 闸全绿。同日独立运行(窗口化与 headless 直跑)八连全绿。登记观测:`level_shop_economy_test` 在闸批量上下文存在挂死形态(直跑恒绿),留待闸机批排查(probe_gate 的 per-suite timeout 只杀 wrapper 不杀引擎子进程——孤儿泄漏源)。 +5. 探针断点段已埋故障指纹打印(仅「战败清断点」偶发失败时输出断点内容指纹)——留作该偶发问题下次复现时的定位面。 + +## 8. 独立验收审计 + +- 审计报告:`reports/iter36-验收审计-2026-10-24.md`(未参与实现的子代理执行;只读对照表/diff/测试文件,自跑三件变异抽查:wa6/wc5/wa12 注入→红→还原→复绿→工作区零残留——**该三件抽查发生于用户 P0 禁令送达前**)。 +- **总判:通过**——wc1–wc7+wa1–wa12 共 19 对照行零缺漏;未申报削弱零(31 行删除逐行归因:27 行产品码钉值+迷雾单行换代 3 行〔§8.3 申报〕+打印消息+头注)。 +- 三件例外均申报/授权在案:wc1 fresh 正向与 wa4 探针半「拦截能力未证」(前提不可驱动——反哺点 ③/⑧)、L2/L3 行值修值(卡C §9 L2)、路径②并入快照喂入形态(代拍板 2+缺陷移交)。 +- 审计留档观察:`_norm_floor_avg` 对部分缺位纹理 continue 跳过(仅全缺必红)——素材齐盘态无实际削弱,备查。 +- **卡③ 审计**:`reports/iter36-验收审计-卡③-2026-10-24.md`——sv1–sv9 全过+契约 1–6 成立+削弱检查零未申报+自跑 theme_projection 162 全绿/ui_struct PASS;唯一缺漏=sv10 产出物缺口,**已补齐**(本节+反哺点 ⑨–⑬+代拍板卡③节),转通过。 diff --git a/reports/iter36-验收审计-2026-10-24.md b/reports/iter36-验收审计-2026-10-24.md new file mode 100644 index 00000000..6f127fc4 --- /dev/null +++ b/reports/iter36-验收审计-2026-10-24.md @@ -0,0 +1,93 @@ +# 迭代 36 独立验收审计报告(2026-10-24)——卡C 开场后缀营地灰字迷雾测点批+卡A 文案与禁字清扫批 + +## 头部:范围、方法与自跑清单 + +- **审计对象**:迭代 36 实现提交 `9468c70e`(19 文件,+489/−31),卡C「开场后缀营地灰字迷雾测点批」(对照表 wc1–wc7)+卡A「文案与禁字清扫批」(对照表 wa1–wa12)。卡③停摆不在范围(另有停机呈报)。 +- **审计人立场**:未参与实现的独立子代理,只读证据、自跑抽查、出本报告;除本报告外零写盘。审计后 `git status -- game/` 复核零残留(工作区仅余实现批四份未跟踪 reports/ 产出物,与本审计前一致)。 +- **输入合同**:`reports/需求卡片-开场后缀营地灰字迷雾测点批-2026-10-24.md` v1.2 §8.2/§8.3/§9 L2/§10;`reports/需求卡片-文案与禁字清扫批-2026-10-24.md` v1.2 §8.2/§8.3。实现报告对照表与变异记录表**不采信**,全部自行核(核后与其叙述相符处才记「与报告相符」)。 +- **方法**:①两张卡 §8.2 逐行核对——断言存在性(grep 尾缀〔wc#/wa#〕)+真实入口未绕改动点+报告差异项授权/申报核对;②变异抽查三件亲自复现(注入→红→`git checkout -- ` 还原→绿→`git status` 复核干净);③削弱检查——实现提交 diff 全部 31 行删除行逐行归因,对照两卡 §8.3 申报。 +- **自跑清单**(Godot=`D:/code/godot/Godot_v4.7.2-stable_win64_console.exe`,工作目录 `D:\code\g-plan2-iter36\game`): + 1. `--headless --script res://tests/unit/presentation/player_copy_sweep_test.gd`(基线绿:24 checks 全绿) + 2. `--headless res://probe/ui_contrast_probe.tscn`(基线绿:40 对全绿+迷雾三关派生校验绿) + 3. `--headless --script res://tests/unit/core/meta_progression_forge_test.gd`(还原后绿:135 checks) + 4. 三件变异注入实跑(见变异抽查实录节) + 5. 静态核:`git show 9468c70e` 逐 hunk 读;`grep` 核 `level_open_line` 签名(五参 bool,未变)、`TILE_FLOOR_FMT`/`TILE_FLOOR_VARIANTS`/`_normalize_floor_luma` 常量与静态函数在位(board_view.gd :68/:69/:998)、条件六项布尔逐字(main.gd `_on_adventure_level_started`)。 + +## 一、逐行核对表——卡C(wc1–wc7) + +| 行 | 判定 | 依据 | +|---|---|---| +| wc1 开场行后缀·新开路径 | **通过(例外申报在案)** | ①静态半:`theme_projection_test.gd` `_smoke_level_open_line` 三断言〔wc1〕在库且绿(true 尾接/false 零后缀/种子两值带入回归);②白板反向:`ui_click_probe.gd` 开场行路径①断言在库(滑条 `reset_sliders()` 显式归零后 `_start_adventure_from_menu()` 真实开局,日志零「(含面板加成)」);③正向半:路径②原设计(奖励节点拿属性卡→下一关)经实查不可驱动,实现并入路径③快照喂入形态。**授权核对**:代拍板 2 白面字面只覆盖「路径③构造细节」,路径②形态改换超出其字面;但实现报告 §0 申报、反哺点 ③ 登记卡片 F5 前提缺陷(L1 零商店节点+reward 节点零兑付直通占位)、停机呈报随附——「或申报」口径成立,且 fresh 机制面(回填→level_started→条件求值→尾接)由续玩路径全链承载+条件 hunk 变异红(mut3,见下注)。fresh 正向拦截能力未证=如实申报事项,非缺漏。 | +| wc2 开场行后缀·续玩路径 | **通过** | 断言在库:探针构造 `bp_ol` 快照(bonus atk=5 已入账、无 bonus_pending/bonus_result 键——缺键钳 0=**选路停点形**钉值满足;node_id=-1 选路停点)→ `main.save_manager.set_breakpoint()` → `main._resume_from_menu()` 唯一消费口 → 断言 run_ledger.bonus.atk==5(resume_adventure :151-154 真链回填钉住)+重建开场行带「(含面板加成)」〔wc1 正向/wc2〕。信号链核:_resume_from_menu→resume_adventure(两键皆空走 :236→_start_current_level)→ level_started → `_on_adventure_level_started`——未绕改动点。 | +| wc3 既有口径回归 | **通过** | 探针首局(滑条 400/200=pct>0 局外加成)开场行带后缀断言〔wc3〕在库,走 `_start_adventure_from_menu()` 真实开局+日志全文本判(该局为探针首局、日志无先前行——判定面干净)。变异 mut4(条件旧四项全删→红)未亲自复现,采信实现报告记录(注入面与断言面一一对应,机制同 mut3 族)。 | +| wc4 营地六处换色 | **通过** | ①六处源位 :167/:417/:476/:509/:513/:525 全换 STONE_DEEP(diff 逐行核);②取色断言在库且读法正确——`get_theme_color("font_color") != UiKit.STONE_DEEP`,经 `open_camp_via_savesel` 真实入口开营地后取(文本锚定位=代拍板 4 白名单内;「未解锁」锚收窄到 `_workshop_holder` 子树避免误捕树行/锻造行——定位写法属 L2 自由);③源 token 扫描断言在 `ui_struct_probe.gd`(STONE 计数==STONE_DEEP 计数判别式)。变异 mut2(:417 回 STONE→取色红)/mut5(同注入→扫描红)/mut6(判别式放宽→误红边界证明)未亲自复现,采信实现报告记录+判别式静态读核(计数对判别式确能区分裸 STONE 与 _DEEP)。 | +| wc5 迷雾测点三行+派生校验 | **通过(行值修值有授权条款+实测记录在案)** | 三行在库(L1 (111,77,51)/L2 (74,79,96)/L3 (71,75,99)),派生校验 `_fog_derived_check`(±2/255)+`_norm_floor_avg`(路径经 `BoardView.TILE_FLOOR_FMT` 常量、归一经 `BoardView._normalize_floor_luma` 静态直呼——契约 3 两禁均守)在库且基线绿。L2/L3 行值与卡片钉值 (79,85,102)/(76,81,105) 不同=卡C §9 L2「手填行值与探针派生首跑失配时的行值修订」预授权,实测记录在案(反哺点 ①+实现报告 §2:PIL↔Godot 解码系统差约 5/255)。三行 ≥4.5:1 由基线跑 PASS 40 对证实。 | +| wc6 注释随动(非行为) | **通过** | `board_cell_unit.gd:270` 段注释换代实核在 diff 内:「归一后(真实渲染面)三关 5.71–5.93:1……见 ui_contrast_probe 迷雾段」——含「归一后」口径与三关值,非行为行,与迷雾绘制本体零触碰。 | +| wc7 反哺点清单(非行为) | **通过(落点形态注记)** | 反哺点清单卡C 三条在库:①art-bible §6.5 换代(含读数修正依据)、②待办清单条目处置、③卡片前提缺陷登记(新增)。卡片 §6 第三行(roadmap 迭代行编入笔)未单列落点——该义务已由 iter-plan 段编入提交(`0e9bc36f`/`30197255` 迭代行已写)履行,实现批以新增前提缺陷条置换该行,实质同步义务闭合,不判缺漏。 | + +**mut3 未复现说明**:卡C 主变异(回退 main 条件 hunk→②③红、①仍绿)涉及窗口化 ui_click_probe 全程跑(约分钟级+已知 1/6 概率静默中断噪声),按审计指令采信实现报告 mut3 记录;补偿核:条件布尔与断言的依赖关系经代码读核(wc1②③ 断言的日志判定只可能因 boosted 布尔含加算区两项而绿——探针该时点 pct=0、新档 ov=0),机制依赖成立。 + +## 二、逐行核对表——卡A(wa1–wa12) + +| 行 | 判定 | 依据 | +|---|---|---| +| wa1 C 区段头×2 | **通过** | sweep 断言①两断言〔wa1〕在库(直建 hero 面板,断 `_feat_section`/`_curio_section` 首 Label text==「专长」/「属性值提升」);探针 C 区断言〔wa1〕在库(`main.info_panel` 可见态取——真实入口);u1/u2 源位 :73/:93 在 diff 内换词。变异(u1/u2 回退→sweep 红+u1 探针红)未亲自复现,采信报告记录(与三件实跑变异同族同法)。 | +| wa2 地图加成帽 | **通过** | 探针地图段断言〔wa2〕在库(地图屏真实开屏后文本锚取「属性值提升」帽);u10 :171 在 diff 内。锚形态=文本锚(L2 定位写法自由);报告申报该断言 mut7 专项实测唯一失败项后复绿。 | +| wa3 专长卡兜底 | **通过** | sweep 断言③〔wa3〕在库:`feat_card_text({})`==「专长\n???」——数据缺行真实兜底路径直调;u11 :142 在 diff 内。 | +| wa4 奖励点头部 | **通过(拦截能力未证=如实申报)** | sweep 断言②六断言〔wa4〕在库(「冒险牌库」「属性值提升与补给」「同库分流」三事实+零「同权重/同上限/提升池」);u3 :156 在 diff 内(u3 措辞与卡片钉值逐字一致)。探针半为**条件断言**(仅捕获到含「关卡奖励点」头部时判)——实跑未捕获奖励点停点,拦截能力未证=实现报告 §1+反哺点 ⑧ 已申报(已知事实不算未申报削弱)。 | +| wa5 图例行 | **通过** | sweep 断言④五断言〔wa5〕在库(含「标空」「受挖掘惩罚」零「插错」「挖雷」);探针图例段断言在库(AP 条图例钮真实信号开层后 `legend_overlay.ENTRIES` 判);u4 :11 在 diff 内。 | +| wa6 状态槽来源名 | **通过(亲自复现变异,见实录①)** | sweep 断言⑤〔wa6〕在库:直建 status_slot_row 喂 `{"source_class":"mine_hit"}` 真实效果行结构→`_on_slot_hover`→tooltip 文本含「来源:危险格」;u5 :40 在 diff 内。 | +| wa7 结算明细来源名 | **通过** | sweep 断言⑥〔wa7〕双半在库:`_db.settlement.source_label_map["mine_hit"]`(ConfigDB 真装载)+直建 battle_log_view `_format_row` 行含「(危险格)」(生产消费路径 :133-136 同构);u6 `settlement.tres:14` 在 diff 内,与 u5 同批同值(契约 1 双源钉住成立)。 | +| wa8 危险格字形族 | **通过** | sweep 断言⑦三半+⑧源扫描〔wa8〕在库:BoardView 直建走 `board_generator.generate`→`minesweeper_rules` 真实规则路径挖掘危险格→`rebuild` 真实刷新路径(theme_projection_test :200 先例式,缺素材程序化回退态与卡片口径一致),断 glyph==「危」/plate==true/glyph_color==RED_MINE;⑧ `board_view.gd` 源零 `"雷"` 字面量(u7/u8/u9 哨兵族统一钉住);u7/u8/u9 三处 :730/:778/:782 同批在 diff 内、:776 注释随动(L1 白名单内)。u8 solo 变异红面=扫描(卡片预判 glyph_color 半红未现)=实现报告 §7.2 如实记录、判据未改,不算缺漏。 | +| wa9 字体「危」在集 | **通过** | `ui_font_asset_test.gd` 增行〔wa9〕在 diff 内;旧四字断言(攻/雷/旗/封)原样(diff 上下文行核);判别力变异(参数换 U+20000→红)采信报告记录。 | +| wa10 golden 判定(非行为) | **通过(判失效路径按契约 4 执行)** | 读数×4 在报告 §6.2(0.13/0.11/0.10/0.00%);加严口径触发→按判失效处理、README 零改动(核:git diff 无 README)、重制与否显式呈报待裁(反哺点 ⑥)+归因证据(master HEAD 复测同读数=存量漂移)。判定先于动作、未先动后判——契约 4 正序执行。 | +| wa11 反哺点清单(非行为) | **通过(落点形态注记)** | 反哺点清单卡A ④⑤⑥⑦⑧五条在库(node-reward/教学引导/golden 纪律句待裁/待办清单退役/卡片前提补记);roadmap 编入笔同 wc7 注记(iter-plan 提交已履行)。 | +| wa12 惊巢反制日志行 | **通过(亲自复现变异,见实录③)** | forge 锋段⑨双断言〔wa12〕在库:编排器真实构造(begin_battle 真实战斗上下文)+`step_row` 信号接线+直调 `_grant_routed` 减半分支,断 display_name==「惊巢反制——危险格挖掘惩罚减半(6→3)」零「挖雷」;sweep ⑩源扫描(注释剥离)在库;u13 :797 在 diff 内(core 触碰面仅此一行字符串——diff 核实)。 | + +## 三、变异抽查实录(三件亲自复现) + +**实录① 卡A wa6(u5 注入)** + +- 注入:`game/presentation/views/status_slot_row.gd:40` `"mine_hit": "危险格"` → `"mine_hit": "踩雷"`(sed 精确替换,diff 核仅此一处)。 +- 命令:`--headless --path . --script res://tests/unit/presentation/player_copy_sweep_test.gd` +- 红(输出摘录):`[FAIL] 状态槽来源注记==「危险格」(u5 换代——meta tooltip 消费半)〔wa6〕` +- 还原:`git checkout -- game/presentation/views/status_slot_row.gd` → 复跑 **all green (24 checks)**;`git status -- game/` 零残留。 + +**实录② 卡C wc5(L1 行值回原图基准)** + +- 注入:`game/probe/ui_contrast_probe.gd:125`(`_fog_derived_check` 行值表)`Color8(111, 77, 51)` → `Color8(69, 48, 32)`。 +- 命令:`--headless --path . res://probe/ui_contrast_probe.tscn` +- 红(输出摘录):`[contrast] 迷雾测点行值失真:L1地牢 行值 (69,48,32) vs 运行时归一派生 (111,77,51)(±2/255——素材更新须随动改行值)` → `[ui_contrast_probe] FAIL — 1 项越带(含静态段)` +- 还原:`git checkout -- game/probe/ui_contrast_probe.gd` → 复跑 **PASS — 对比度 40 对测点全绿……+迷雾三关派生校验绿**;`git status -- game/` 零残留。 +- 评注:注入点选在派生校验行值表(行值钉值面),证明「行值回原图基准必红」的变异①判据真实有拦截力;同法覆盖卡C T3-①。 + +**实录③ 卡A wa12(u13 注入)** + +- 注入:`game/core/battle_orchestrator.gd:797` 「危险格挖掘惩罚减半」→「挖雷惩罚减半」(sed 精确替换)。 +- 命令:`--headless --path . --script res://tests/unit/core/meta_progression_forge_test.gd` +- 红(输出摘录):`[FAIL] 惊巢反制效果行==「惊巢反制——危险格挖掘惩罚减半(6→3)」零「挖雷」(u13 换代)〔wa12〕` +- 还原:`git checkout -- game/core/battle_orchestrator.gd` → 复跑 **all green (135 checks)**;`git status -- game/` 零残留。 + +三件均满足「注入→最窄闸见红→精确还原→复绿→零残留」闭环。未亲自复现的其余变异(卡C mut2/5/6、mut3、mut4;卡A 十二注入中未抽查件)采信实现报告记录,其判据面均经静态读核与实跑同族推断,无发现虚报痕迹。 + +## 四、测试削弱检查结论 + +实现提交全部 31 行删除行逐行归因(`git show 9468c70e | grep '^-'"`): + +1. **产品码钉值行 27 行**——u1–u13 钉值表逐字+main 条件旧式三行+camp 六处 STONE+board_cell_unit 注释+board_view :776 注释随动:全部为两卡 §1 钉值表/契约明文授权的换代对象,非断言。 +2. **迷雾单行 3 行**(ui_contrast_probe 旧「迷雾·记忆墨/灰罩合成底」测点)——**卡C §8.3 申报的换代删除**,替换判据更严(三关归一基准+派生一致性校验),申报成立。 +3. **探针 PASS 打印行 1 行**——输出消息随断言面扩段更新,非断言、非闸判据。 +4. **theme_projection 头注 1 行**——「十套」→「十一套」注释行,非断言。 + +**专项核**:`theme_projection_test.gd` 只增不改(新增 `_smoke_level_open_line`+调用行+头注,既有断言零触碰);`ui_font_asset_test.gd` 旧四字断言原样、只增「危」行;`adventure_feats_test.gd` 未触(不在 19 文件清单);`ui_click_probe.gd`/`ui_struct_probe.gd`/`meta_progression_forge_test.gd`/`run_all.gd` 全为纯增行;零阈值放宽、零扫描面缩小、零真实入口换下层直调(探针新增段全走 `_start_adventure_from_menu`/`_resume_from_menu`/`open_camp_via_savesel`/图例钮/选路真链/`_grant_routed` 真实链)。**未申报削弱:未发现。** + +**一处观察(不判缺漏,随报告留档)**:`ui_contrast_probe._norm_floor_avg` 对缺位纹理 `continue` 跳过(仅 px==0 全缺时必红)——卡片契约 3 写「素材缺位=按闸预检处置拷贝齐盘,禁跳过派生校验」。实现仍执行派生校验、未跳过校验本体,部分缺位时以余量均值参与比对(大概率仍因均值漂移超 ±2 而红,但非构造保证)。闸预检+素材在盘现状下无实际削弱,属 L1/L2 级实现细节,登记备查。 + +## 五、总判 + +**通过。** + +- 卡C wc1–wc7、卡A wa1–wa12 逐行「通过/通过(例外申报在案)」,无缺漏行。 +- 例外三件均属「申报+授权在案」而非缺漏:①wc1 fresh 正向与 wa4 探针半「拦截能力未证」(前提不可驱动——反哺点 ③/⑧ 已登记);②L2/L3 行值修值(卡C §9 L2 条款+实测记录在案);③路径②形态改快照喂入(代拍板 2 授权路径③构造、路径②合并超出其字面但已申报为卡片前提缺陷,fresh 机制面由续玩路径全链承载)。 +- 三件变异亲自复现全红、还原全绿、工作区零残留;削弱检查零未申报项。 +- 闸回执(commit=9468c70e)8 闸 green;11 回放确定性对拍 NEED-APPROVAL 为存量红零新增、未实跑——是否补跑归用户,不在本审计判据内。 diff --git a/reports/iter36-验收审计-卡③-2026-10-24.md b/reports/iter36-验收审计-卡③-2026-10-24.md new file mode 100644 index 00000000..4b4f8204 --- /dev/null +++ b/reports/iter36-验收审计-卡③-2026-10-24.md @@ -0,0 +1,57 @@ +# 迭代 36 验收审计——卡③「状态面板瘦身批」(2026-10-24) + +- 审计人:独立验收审计子代理(未参与本迭代实现;只读审计,唯一产出物=本报告) +- 审计对象:分支 `iter36`,diff 基线 `2931b1f2..HEAD`(477a73e7 卡③实现笔+8a37712e 静态扫描归零笔);卡片 `reports/需求卡片-状态面板瘦身批-2026-10-23.md`(v1.1) +- 断言宿主指认:按用户裁决采迁移后现址执行(`game/smoke/` 已于 `16c6f136` 退役)——sv1–sv5/sv8 隔离档现址=`game/tests/unit/presentation/theme_projection_test.gd` `_status_panel_projection`;sv6/sv7 真实入口现址=`game/probe/ui_click_probe.gd`。宿主变迁凭据=`reports/iter36-停机呈报-状态面板瘦身批-2026-10-24.md`+迁移账本 S61 +- 方法:逐行核对卡片 §8.2 对照表 sv1–sv10(断言存在性+尾缀 grep+真实入口形态+契约逐条读 diff);正向自跑抽查(本项目已明令禁止变异测试,零注入零回退,只跑绿) + +## 1. 对照表 sv1–sv10 逐行判定 + +| 行 | 判定 | 依据 | +|---|---|---| +| sv1 属性行投影 | **通过** | 断言在座且尾缀可 grep:theme_projection_test「属性行全值投影 = 契约4 钉值格式逐字(生命/上限/攻击/AC/GP 五现值)〔sv1〕」(现址 `_status_panel_projection` 段内),夹具钉值 13/29/7/11/41 全值逐字比对「生命 13/29 · 攻击 7 · 护甲 AC 11 · GP 41」,自跑绿。真实入口侧另有探针 sv6② 全值对 runner 五现值(见 sv6 行)。隔离档为八键手拼夹具直喂 `open_status`(同构口径、不依赖 runner),与卡片口径一致 | +| sv2 秘宝行投影 | **通过** | 断言两则:「秘宝行投影 = 受理序 join+已用件「(已用)」尾缀〔sv2〕」(期望串「青苔密卷 · 锈蚀怀表(已用)」——受理序+尾缀逐字)+「空秘宝态句=「未携带秘宝」〔sv2〕」,自跑绿。投影复用 `_treasure_ui.inventory_projection()`(main.gd `_status_curios` 只做 name/used 键裁剪,零第二份件名换算) | +| sv3 专长行投影 | **通过** | 断言两则:「专长行投影 = 行串 join〔sv3〕」(期望「疼痛记忆 ×2 · 双管镐 ×1」)+「空专长态句=「未获专长」〔sv3〕」,自跑绿。feats 取 `HudPresenter.feat_holdings_lines(db, runner.feats_held())` 现成行透传(main.gd `_status_data`),零第二份计数实现 | +| sv4 余额徽回归 | **通过** | 断言「余额徽投影 = 快照 balance 读数(sv4 回归锚)〔sv4〕」在座(沿旧断言族改喂夹具钉值 77),自跑绿;`open_status` 余额徽喂值逻辑零改动(diff 核:仅 `data.get("balance", 0)` 原样保留) | +| sv5 零旧段(隔离档) | **通过** | 断言两则:「面板子树零「道具工坊/天赋树」文案(跨关概览退役零残留)〔sv5〕」(`_all_labels` 递归全子树扫描)+「旧段成员退役(_workshop_holder/_tree_label 零残留——契约2 数据/成员层面清零)〔sv5〕」,自跑绿。源内核:status_panel.gd 零 `_rebuild_workshop`/`_rebuild_tree`/`_workshop_holder`/`_tree_label`(grep 零命中);段头/行构建/数据键三层面清零。**留档观察(非缺陷)**:文件头 `##` 注释第 3 行含「天赋树已购」字样——系退役去向说明句(「由营地/生涯承载,本面板不镜像」),非面板子树 UI 文案、非段头,sv5 机检扫描面(Label 文本)不含注释,判不违契约 2 | +| sv6 零旧段+投影值真链路(真实入口) | **通过** | 三断言全部在座于 `game/probe/ui_click_probe.gd` `_run_modal_probes_once` 状态面板段(:754-810 现址),且该段由 `main.board_zone.status_badge.pressed.emit()`(:763,真实按钮信号路径→组合根接线)开启面板后执行——真实入口未绕过。断言①=子树零「道具工坊/天赋树」文案(问题串带「sv6①」);断言②=属性行全串==契约 4 格式 % `run_scope.runner` 五现值(hero_hp/effective_max_hp/effective_atk/effective_ac/gp——问题串带「sv6②」,runner 空指针单独报契约 5 缺陷禁软回退);断言③=`main._status_data()` 直调八键键集+curios 逐行 {name, used} 对拍独立 oracle(`runner.treasure_inventory.query_inventory(Callable())` 行集 × `main.db.treasure_item_row().display_name`,件名换算不走被测投影)+feats 对拍 `HudPresenter.feat_holdings_lines`(问题串带「sv7」)。未实跑探针(窗口化长跑,按审计指令采信实现报告问题=0 读数;三断言代码存在性与真实入口形态已静态核成立) | +| sv7 投影零旧键 | **通过** | 承载=sv6 断言③键集恰八键(size==8+逐键 has)+键序无关;diff 核 `_status_data()` 现 return 恰 balance/hero_hp/max_hp/atk/ac/gp/curios/feats 八键,workshop/tree/fork_groups 三旧键清零;全 presentation 检索旧三键仅剩营地侧消费(camp_shop_presenter.gd/camp_overlay_view.gd——契约 2 明示营地在役件,零关联)。直调发生在探针对局中(runner 在役) | +| sv8 提示行文案 | **通过** | 断言「提示行=「查看本局冒险进度(只读)」(新定位换代)〔sv8〕」在座,自跑绿;diff 核 status_panel.gd 提示行换代逐字;标题「状态」与「继续」钮零改动(diff 核+struct/click 探针既有定位锚回归绿) | +| sv9 角标 tooltip×2 | **通过** | diff 核两处换代逐字:board_zone_view.gd:58/level_map_screen.gd:107 →「查看本局冒险进度(只读)」;全仓检索旧串「查看钥石与永久强化进度」零命中、「永久强化进度」仅剩 theme_projection_test.gd:786 一处测试注释(非玩家面文案,判不违)。sv9 断言宿主=件核+四探针回归(卡片 §8.2 原口径),四探针本批实跑全绿(实现报告 §6.1+本审计 struct 探针复跑) | +| sv10 反哺点清单(非行为改动) | **缺漏** | 见 §4——三份 `reports/` 产出物均未覆盖卡③:反哺点清单卡③节仅 ⑨ 停机呈报移交一条,卡③ §6 六条同步面(元进度①/生涯②/08-Alpha③/体验验证清单④/待办清单⑤/roadmap⑥)零落点;实现报告自记「本报告只覆盖卡C/卡A」,卡③状态总表/变异禁令申报/目检总清单「面板观感」条均缺;代拍板追认单无卡③节(§10 白名单五项——空态句措辞/属性行串接序/断言定位写法/夹具数值取值/提交切分——本批均有行使,未备案) | + +## 2. 契约核查结论(读 diff 逐条) + +- **契约 1(五件口径/禁第二份投影)**:成立。面板四块=钥石总量徽+「本局状态」合一行+「带的秘宝」段+「已获专长」段,零新增段;属性/GP 取数=LevelRunner `hero_hp`/`gp` 属性(level_runner.gd:30-36,getter `int(_ledger["hero_hp"])`/`int(_ledger["gp"])` 直取下标——键缺失即致命,符合「禁 .get 缺省留门」)+三 effective 访问器;秘宝=`_treasure_ui.inventory_projection()` 逐行键裁剪;专长=`HudPresenter.feat_holdings_lines` 现成行。零第二份投影/拼装。 +- **契约 2(两段退役零残留)**:成立。status_panel.gd 源内 `_rebuild_workshop`/`_rebuild_tree`/`_workshop_holder`/`_tree_label` 及两段段头文案全清(grep 零命中;头注「天赋树已购」为退役去向说明句,见 sv5 行观察);`meta.treasure_shop_status()`/`tree_camp_rows()`/`tree_fork_groups()` 三投影函数本体零改动(`git diff 2931b1f2..HEAD -- game/core/meta_progression.gd` 为空);camp_overlay_view.gd 本批改动仅卡C wc4 灰字换色六处,其同名 `_rebuild_workshop`/`_rebuild_tree` 本体未动。 +- **契约 3(只读快照/UiKit 工厂/零动效)**:成立。open_status 单次开层重建口径沿袭、跨开层零持有、零写调用;新三 Label 全经 `UiKit.label` 工厂产出,零裸色值;自跑 `ui_struct_probe` PASS——⑧ 只读契约(信号连接数=2,白名单 `_emit_closed`/`_on_status_closed`)与⑨ 零动效契约(子树零 AnimationPlayer+源码零 `create_tween`/`UiKit.motion`)对重写后面板仍绿。 +- **契约 4(文案钉值)**:成立。提示行/两 tooltip=「查看本局冒险进度(只读)」逐字;段头「本局状态」「带的秘宝」「已获专长」逐字在座(源核);空态句「未携带秘宝」「未获专长」逐字(机检断言在座);已用尾缀「(已用)」逐字(机检断言在座);标题「状态」/「继续」钮不变。**留档观察**:三段头无正向机检断言(卡片 §8.2 断言清单未列段头项,隔离档与探针的 sv5 扫描只反向钉旧段头)——呈现层唯一载体的段头文案现仅由源核与本审计覆盖,判不违卡片断言清单,登记备查。 +- **契约 5(runner 在役零软回退)**:成立。`_status_data()` 直读 `run_scope.runner`,无 null 守卫无缺省回退;探针侧 runner 空指针显式报「契约5——状态机缺陷」问题项,不掩盖。 +- **契约 6(数值单一事实源)**:成立。面板与投影零数值字面量(仅文案格式串),全部实时读访问器现值;生命上限走 `effective_max_hp()` 生效上限口径。 + +## 3. 削弱检查(test-conventions §9.5 申报核对) + +1. **theme_projection_test 删除的旧断言族即 §8.3 申报范围**:本次 diff 删除的旧内容=meta 存档夹具+工坊 12 行投影逐项断言块+天赋树聚合文案断言块;保留=开层可见断言(原样)+余额徽断言(改喂夹具钉值,sv4 回归锚)。经 `git show 16c6f136~1:game/smoke/smoke_all.gd` 调出迁移前原件逐段比对:迁移后(2931b1f2 时点)theme_projection_test 的 `_status_panel_projection` 与 smoke_all 原件内容实质逐句相同(仅 check→Kit.check 宿主化),即删除面=迁移时保留至今的 §8.3 申报两段(工坊 12 行+天赋树聚合),无额外断言被删。§8.3 申报的保留区间(开层可见+余额徽)确实保留。宿主现址执行经用户裁决(停机呈报移交),申报边界按迁移后实况成立。 +2. **ui_struct_probe/ui_input_probe/shot_probe 零断言改动(卡③口径)**:成立。三文件在本批 diff 中,ui_input_probe/shot_probe 零改动;ui_struct_probe 仅 +5 行且全为卡C wc4 营地灰字源 token 扫描段(camp 域,与状态面板断言零交集)——卡③自身对三探针零改动,回归线全绿。 +3. **变异验证零执行**:卡③六条注入实验(T1 三条+T2 三条)未执行——用户 P0 禁令明令禁止变异测试,本审计亦零注入。替代拦截证据=sv1–sv8 全部正向机检断言在座且自跑绿(sv6② 钉格式逐字对 runner 现值、sv7 键集+独立 oracle 对拍,值层有判别力);但「撤销该改动能让断言变红」的变异证明在卡③范围未成立,按禁令口径记「已实现,拦截能力未证(变异验证按用户 P0 禁令零执行)」。此例外须随卡③补进实现报告(并入 §4 sv10 回炉项)。 + +## 4. sv10 缺口明细(唯一缺漏,回炉 T2 产出物) + +1. `reports/iter36-反哺点-2026-10-24.md` 卡③节现仅 ⑨ 一条(停机呈报移交);卡③ §6 六条同步面(元进度.md「已解锁清单」句①/生涯.md 两处「解锁进度」②/08-Alpha「部分在役」销号③/体验验证清单 ⑳ 追溯改写④/待办清单条目批后退役⑤/roadmap 迭代行⑥)每条须一落点,现零落点。sv10 判据「§6 六条每条有落点〔sv10〕」不满足。 +2. `reports/iter36-实现报告-2026-10-24.md` 需补卡③节:状态总表(对照完成定义五项)、变异禁令申报(§3.3)、实机目检总清单「面板观感」条、改前改后读数对拍(theme 153→162 本审计实测在案,可采)。 +3. `reports/iter36-代拍板追认单-2026-10-24.md` 需补卡③节:§10 白名单行使备案(空态句/段头最终措辞、属性行串接序、断言名与定位写法〔含断言宿主重锚落点选择〕、夹具数值取值 77/13/29/7/11/41 与空态夹具、提交切分两笔)。 + +## 5. 自跑实录(正向抽查,cwd=D:\code\g-plan2-iter36\game) + +1. `Godot_v4.7.2-stable_win64_console.exe --headless --path . --script res://tests/unit/presentation/theme_projection_test.gd` → **all green (162 checks)**,含 sv1/sv2×2/sv3×2/sv4/sv5×2/sv8 九断言全 PASS(退出期 RID 泄漏告警为 headless 存量噪声,与本批无关)。改前基线 153(卡C 报告 §6)→ 162,与卡③九断言增量吻合。 +2. `--headless --path . res://probe/ui_struct_probe.tscn` → **PASS — 全屏结构断言零红**(exit=0),⑧ 两契约(信号连接数=2+零动效 token)对重写后面板仍绿;输出中「Parse JSON failed/broken」为探针坏档夹具既有噪声。 +3. `ui_click_probe` 未实跑(窗口化长跑,按审计指令采信实现报告「问题=0」读数;三断言代码存在性、真实入口形态〔board_zone.status_badge pressed→组合根接线→断言段〕、独立 oracle 取数面已静态核成立)。 +4. 审计期间工作区存在一笔未提交改动(ui_click_probe.gd 战败断点段诊断打印,卡② T3 域,非卡③范围)——不影响本次审计结论,登记备查。 + +## 6. 总判 + +**不通过(仅因 sv10 产出物缺口;代码面与断言面全部通过)。** + +- sv1–sv9 九行全部「通过」:断言在座、尾缀可 grep、真实入口未绕过、契约 1–6 逐条成立、削弱面与 §8.3 申报逐行吻合(迁移史比对成立)、隔离档与 struct 探针自跑全绿。 +- 唯一回炉项=sv10:三份 `reports/` 产出物补齐卡③覆盖(§4 明细三条)后复判——补齐并经确认即可转「通过」,代码零回炉。 +- 合并前置(卡 §7 T2「审计逐行通过才合并」)在本报告转通过前不满足。 diff --git a/reports/审计-game代码结构审查-2026-10-24.md b/reports/审计-game代码结构审查-2026-10-24.md new file mode 100644 index 00000000..1db957fd --- /dev/null +++ b/reports/审计-game代码结构审查-2026-10-24.md @@ -0,0 +1,116 @@ +# 审计:game 代码结构审查(2026-10-24) + +> **性质**:设计讨论段过程稿(审计读数)。`game/` 全目录只读结构审查的发现清单与证据,作为写卡段与非迭代批的输入材料;结论被吸收进代码修复与权威文档后按吸收即死退役。 +> **范围**:`game/` 全目录 194 个 .gd、约 4.5 万行(tests/probe/sim 不在产品码判定面)。基准双轨:项目自有规范(`design/engineering/architecture/architecture.md` 事实源 v1.9 + `design/engineering/code-conventions.md`)+ Godot 4.7 最佳实践。 +> **方式**:核心玩法域/组合根层/views 层三路深读+战斗结算域与数据基础设施层逐行通读+机械 grep 验证。**零测试执行**(测试两入口铁规),全部结论来自静态阅读;S9 正则失灵结论经独立复现(python 逐条对拍)。 + +--- + +## 一、总评 + +架构质量在同类个人项目里属罕见的高水位:五层分层(core 纯逻辑 headless 可跑/presentation 组合根)、B1 编排唯一性、RNG 封闭流、数值单一事实源、效果引擎四词表封闭、可回放确定性——这些约定不仅写在文档里,代码里也确实兑现(逐条 grep 验证:core 层零 `await`、零裸随机、`load()` 只在 config_db 单点、无 get_parent 链、views 层零核心写调用)。设计模式选型全部贴合领域:组合根、门面(ConfigDb)、发布订阅解释器(EffectEngine)、单据管线(DamageController)、快照/备忘录(断点续玩)、令牌池。 + +**真正的风险不在玩法代码本身,而在三处:保障层有洞(S9 类型闸正则失灵、S6/S7/S8 未落地)、规范与实现之间漂移累积(写调用位置、行类型词表、模块归属表)、以及一批真实但低频的行为缺陷。** 另有约 10 个文件超过项目自设的 ~400 行审视线,其中 main.gd(1508 行)已超过架构文档自记的 1100 行上帝对象反面案例。 + +--- + +## 二、Critical——会出 bug 或违反硬性契约 + +### 保障层(2 件) + +**C1. S9 类型闸正则两处失灵,R2 从上线起恒零命中**(`tools/audit_type_hints.py:29`,经独立复现) + +- `FUNC_DECL` 正则 `^(\s*)func\s+(_?\w+)\s*\((.*)\)\s*(->\s*.+)?\s*(#.*)?$` 两个洞:① 无返回类型的声明按 GDScript 语法必然以 `:` 结尾,正则要求 `)` 后只能跟 `->`/注释/行尾——**R2 的目标形态 100% 失配跳过,R2 恒零命中恒绿**;② 不认 `static ` 前缀,static 函数的 R2/R3 整体漏网。 +- 实证存量(闸当前退出码 0 全绿):`game/core/level_runner.gd:632`(`current_battle()` 返回 BattleOrchestrator 无 `->`)、`game/core/attunement.gd:10`(`static func slots(db, save)` 两参无类型)、`game/presentation/main.gd:565`(build_adventure_params 两参)、`game/presentation/views/hud_presenter.gd:10-11`(refresh 三参)、`game/core/reward_economy.gd:104/119/131/146`、`game/core/unit_container.gd:275`。 +- 这正是架构文档测试域自警的「规则扫描面为空=恒零命中恒绿」静默失效模式;零命中自检没拦住(因 R1/R3 仍有命中能力,整体不空)。修复义务按 test-conventions §6:正则修后先跑出存量红,补注解或 `# type-hint: ignore` 归零,闸改动配变异验证。 + +**C2. S6/S7/S8 三条静态规则未落地**(static_scan.sh TODO 在案,架构文档静态扫描表自认):B1 编排唯一性(B2/B3/B4 API 符号命中 ⊆ B1 文件集)、结算路径禁 await、core↔core 信号禁 lambda 捕获。本次人工 grep 结论干净(core 零 await、A2/B3/B4 零相互直调),但防线缺位=回归无闸;S7 落地前须先在架构文档成文 B1 文件集边界(含 `game/core/settlement/` 五段类与 damage_controller——现归属表零条目)。 + +### 架构契约(3 件) + +**C3. 基础层 A3 反向依赖核心层 EffectEngine**(`game/core/action_economy.gd:43-49,105,166,193` 经 `set_fx` 持 EffectEngine 并 dispatch;B4 `game/core/reward_economy.gd:24-31` 同款但属同层调用)。架构原则 6「依赖单向、上层可消费下层禁止反向」,且架构文档 A3 行消费依赖列未登记 EffectEngine——**真违约与事实源失真二选一,需裁决**:a) effect_engine 降登记为基础层(零状态纯判定,形态本就近基础层服务);b) A3 的 dig_paid/dig_stop/charge_full 发射改由 B1 调用前后代发,A3 回归纯账本;c) 最低限度先把消费依赖登记进架构文档消掉漂移。 + +**C4. 核心写调用外迁三控制器,code-conventions §1.3 字面已漂移**(spec-AC 冲突)。规范写「核心写接口(settle_batch/submit_window_selection/runner 推进)只允许在组合根出现」,实况:`game/presentation/curio_interact_controller.gd:76-244`(continue_after_node/pick_curio/apply_node_curio 等 6 口)、`game/presentation/camp_shop_presenter.gd:175-212`(buy_pool_item/shop_refresh 等 4 口)、`game/presentation/treasure_interact_controller.gd:89`(use_treasure)。视图层本身干净(views/ 零写调用,grep 实证)——漂移发生在「组合根构造注入的拆件控制器」这个**未成文层**,三控制器头注各自把「组合根唯一」重定义成「零直写」,与 §1.3 是两套口径。修法二选一:改规范成文「组合根家族=main.gd+三拆件控制器(组合根构造注入、禁视图层直连)」;或推进/购买调用经回调收口回 main 执行。顺带:§1.3 例举的 `submit_window_selection` 符号在 presentation 已不存在(现行面经 settle_batch/player_request_refresh 承载),例举已过时。 + +**C5. 明细行类型封闭枚举漂移**:编排器头注列 9 类(`game/core/battle_orchestrator.gd:15-19`),core 侧实际发射 13 类(grep 实证多出 `fx_reveal`/`mine_neutralized`/`insurance_used`/`fx_preview_area` 四类),架构文档 B1→D1 行又写 15 类——三处三个数。头注自称「清单改动须同步 battle_log_view 的 _format_row 与本头注」的同步义务已失效。 + +### 行为缺陷(4 件+1 件半 Critical) + +**C6. `game/core/damage_controller.gd:215` 用 `==` 比较单位字典**——Godot 4 的 Dictionary `==` 是逐键内容比较而非引用比较。两个内容全同的敌人(同种同拍生成、同待发技能、同池状态),其一死亡会经 `_on_unit_died` 误作废对方的排队单据(「死亡即不动手」误伤存活敌)。修法:`is_same(t.source_unit, u)`。该路径无测试覆盖同名敌作废面。 + +**C7. standee 蓄力意图动画档永不触发(常量错配死代码)**:`game/presentation/views/standee_body.gd:172` `set_pending` 落 `UiKit.GOLD_INK`(116,80,11),`:188` `_play_intent_motion` 判 `UiKit.GOLD_DEEP`(176,122,20)——③「蓄力缓推+定格微顿+回落」分支恒不可达,三档运动签名互异的设计失效;`game/presentation/views/ui_kit.gd:28` 注释「GOLD_DEEP 语义岗在役不动」已随换墨漂移成错误陈述。修法:判档不靠颜色相等(本身脆弱),set_pending 落 `_intent_charge` 布尔按其分档;修后补「charge 意图触发蓄力档」断言(现测试面未覆盖此档)。 + +**C8. `game/presentation/views/battle_end_overlay.gd:281-287` 庆祝浮动 tween 无限叠加泄漏**:`set_loops()` 无限循环 tween,`close()` 只隐藏不 kill——每次胜利 open 都新增一个永不停止的 tween,多 tween 争夺同一 position 目标值产生视觉抖动且随局数无限累积。全库其余 tween 均有先 kill 惯例(board_view:362-364 先例),此件唯一漏网。修法:存 tween 句柄,close() 与 open_victory 开头 kill。 + +**C9. 批量模拟与两层调试互斥未闭合**:`game/presentation/views/debug_overlay.gd:84`(调试钮不看 `_sim_running`)+`game/presentation/main.gd:712-715`(`_start_debug_levels` 守卫漏模拟运行态,且 :717 reset_sliders 会作废在跑测量的滑条读数)+`game/presentation/main.gd:1489`(`_on_tune_simulate` 守卫漏 `is_adventure`)+`game/presentation/tune_sim_controller.gd:46-58`(逐帧整关同步跑、与在跑战斗并存=帧时长尖峰扭曲 RowPacer 排播与 Tween 演出)。已核实缓解面:autoplay 自建局部 RngService(`game/sim/level_autoplay.gd:44`),无全局 RNG 竞态。修法:调试钮 disabled 并入 `_sim_running`;两守卫互查运行态(TuneSimController 补 `is_running()`);后者补 `or run_scope.is_adventure`。 + +**C10(半). `game/presentation/modal_stack.gd:23-29` 对未登记层与重复开层静默降级**:`_list.find(m)` 为 -1 时 range(0,size) 会隐藏全部已开层后显示未登记层;同层重复 open 时 `_occluded[m]` 被空表覆盖,原被隐藏层永久卡隐。当前调用面无违规者(badge 置灰+is_open 检查守住),属埋雷,但「层必须先 register 方可 open/close」只写在头注、未机器强制——违 fail-fast 原则 5。修法:open() 首行 find<0 即 push_error 且 return;同层已 visible 幂等直返;close() 对未登记层同样 push_error。 + +### 已核实的其余缺陷定点(写卡面) + +- `game/core/minesweeper_rules.gd:8-9` 两条核心信号(cell_revealed/seal_judged)零契约成文,违 code-conventions §2.1;同类已成文的(round_end_board 等)用的是 `#` 而非规范要求的 `##`(不进编辑器 F1 帮助)——全库信号注释载体普遍 `#`。 +- `game/core/level_runner.gd:895`(relic 基价 15.0)与 `:779`(散折 0.8)是玩法数值字面量,各自注明 provisional/待追认——仍违数值单一事实源红线,应入表。 +- `game/presentation/views/battle_stage_view.gd:332-338` 以 display_name 子串匹配解析受击站桩(`_card_by_name` 的 contains)——同名敌人或名字互为前缀会错配,弹道/闪白/击杀打到错误立绘。修法:结算行携带 enemy_id 或槽位 index 按键解析。 +- `game/core/treasure_items.gd:56-65` 孤儿注释指向已删函数(三段「持有数/持有件记录/丢弃」描述的函数连空壳都不在)。 +- `game/core/board_generator.gd:97/334` 目标雷数公式两处手抄;`:281` legality 的 db 参数未用。 +- `game/core/action_economy.gd:191-192` 蓄力层上限 3 硬编码两处字面量。 +- `game/core/unit_container.gd:55` 英雄名「探险者」硬编码绕开 HeroTable(敌人名走 row.display_name)。 +- §4.2 Dictionary 取值 Variant 裸算术漏包 int():`game/core/unit_container.gd:257/270`、`game/core/status_container.gd:58-61/102/114`。 +- `game/presentation/views/settlement_fx_view.gd:94-95` await process_frame 后无 is_instance_valid 守卫(与全库守卫惯例不一致)。 +- `game/presentation/views/board_view.gd:998` `lumas_out` 参数缺类型且无豁免注记(S9 R3 面存量)。 + +--- + +## 三、Improvements——结构与可维护性 + +### 大文件职责评估(均超 ~400 行审视线) + +| 文件 | 行数 | 判读 | +|---|---|---| +| `game/presentation/main.gd` | 1508 | 拆件真实发生(13+ 控制器/装配器),但数据投影族(L1059-1205,~135 行)、只读解析回调(L755-798)、文案拼装三件可判离合计 ~440 行;拆后回 ~1070 行。已超架构文档自记的 1100 行上帝对象反面案例 | +| `game/core/battle_orchestrator.gd` | 1301 | 段类拆出后仍持同调管理(~150 行)、变异布置算法(L516-574)、授予路由族(~200 行)、双快照面。「B1 退为段序编排+接线」文档目标只兑现一半 | +| `game/presentation/views/board_view.gd` | 1219 | 至少五职责混装(格面渲染/输入翻译/英雄动画/瓦片算法/探针面);最划算两刀:瓦片静态算法+哈希查表(~230 行无状态,拆 TilePalette 静态装配器)+英雄精灵状态机(~110 行) | +| `game/core/adventure_runner.gd` | 1049 | 令牌池子系统(构建/抽签/cap/凭证,~500 行)自成一体可拆 CurioDeck 类;**断点 schema 三处重复登记**(_snapshot_breakpoint 写/resume_adventure 读/save 默认值),每加一字段改三处,散弹式修改热点 | +| `game/core/level_runner.gd` | 1012 | 商店子系统(legacy+pool 双模式,~260 行)可拆;pool_price 字面量见上节 | +| 其余超线件 | 612-848 | ui_kit.gd 848 行属 §1.2 明文允许的静态装配器宿主(内聚成立);level_map_screen 612/camp_overlay_view 555/standee_body 491/save_manager 698/damage_controller 628/meta_progression 563/node_map_generator 539 各有内聚理由,未见上帝对象 | + +### 结算五段模式的形态说明(设计观察,非违约) + +段类是「宿主引用+直访宿主私有成员」的拆函数形态——`game/core/settlement/grant_segment.gd` 对 `oc._facts_reveals`/`oc._fx_bonus_delta`/`oc._defer_board_damage` 等私有成员访问达数十处。架构文档「段类可直接 new() 单测」技术上成立(settle_segments_test 在役),但段不是独立单元而是编排器的显式分部;这是有意权衡(零行为变更契约、行发射/开单单点归编排器),文档与代码一致,呈报确认即可。附带小件:`battle_orchestrator.gd:314-321`「遭遇骰」注释整段重复两遍;hp_ratio 公式三处手抄(grant_segment:91/145、orchestrator:188);`_place_variants`(L516-574,region 布置算法)住编排器,生成期逻辑归 A1/A4 域更顺——是否搬迁留重构批裁决。 + +### 文档-实现漂移清单(走文档轨对齐批) + +1. 架构文档「模块归属」多行 API 清单与实现不符:A2 行 try_dig/try_flag_place/try_flag_pull/try_move 不存在;A3 行 request_flag_place/request_flag_pull 不存在(实为 try_spend 通道),实有 grant_reserve_ap/finish_round_stop 未登记(:226 注释自称「第 9 API 接口漂移预授权」——registry 有授权、架构文档没跟);B2 行 ambush_grant_plan 不存在、实有 check_skill_unlocks 未登记;B4 行 equivalence_supply 不存在;damage_controller 与 settlement/ 五段类在归属表零条目。 +2. 架构文档 :149/151 层合同计数写死「17 项」,main.gd LAYER_CONTRACT 实为 18 项(文档明令「清单不复制」却把计数写死——双写失同步变体)。 +3. 明细行协议三处三个数(见 C5)。 +4. code-conventions §1.3 例举 submit_window_selection 已退役(见 C4)。 +5. `game/core/effect_engine.gd:360` 校验报错文案「事实词汇封闭 9 项」实为 18 项。 +6. A3/B4 消费 EffectEngine 未登记(见 C3)。 + +### 其余小件 + +- `game/presentation/main.gd:1215` `_sync_status_entry` 死变量 battle_live+四句重复(注释自认);:1194/498/507 confirm_dialog 旁路 `_open_modal/_close_modal` 单口(违 main.gd 自身头注 L4),confirm_dialog.gd:40-41「树序由构建序保证」注释已失真。 +- `game/presentation/views/level_map_screen.gd:326-353` 视图直读 LevelRunner 拼文案(§1.3 唯一越界者——「读核心状态→拼展示文案应集中装配器」);:362-387/520-529 每次鼠标移动全图 BFS 重算+O(V×E) 线性扫(当前图规模无实感影响);:502/507/515 绘制循环内重复取主题字体。 +- `game/presentation/main.gd:1276/1303` settle_batch 前置空判隐式依赖 `_input_locked` 锁谓词第三例外——入口显式 null 防护零成本双保险。 +- `game/presentation/row_pacer.gd:11` 待播队列住 main 级常驻节点(架构 L145「局态一律住 RunScope」),会话更替零残留靠时序约定兜底——`_begin_session` 内 flush 一行变机制保证。 +- core 域 `_fatal` 的 `assert(false)` 在 release 模板剥离后退化为软回退(orchestrator 有 battle_result 封印兜底、`game/core/board_generator.gd:391-393` 与 `game/core/reward_economy.gd:366-368` 无)——出包前需换真中止(可经宿主注入 fatal 回调)。 +- `game/core/rng_service.gd` open_stream 未知标签 push_error 后未 early-return(release 剥 assert 后静默继续发号)——补 return 即闭合。 + +--- + +## 四、Positive——做得好的,应当保持 + +- **分层纪律全兑现**:core 全域 RefCounted/Node 零 Control/场景引用、零 await、headless 可跑;views 零核心写调用、信号单向外发、接线集中在 main.gd 一处;A2/B3/B4 零相互直调(grep 实证)。 +- **RngService 是全库最好的设计**:单一 master_seed+派生混淆(splitmix64 finalizer 抗窄种子窗)+全局计数器快照契约+封闭用途词表+事件日志+pure_stream 纯函数面(结构生成与号位解耦)——断点续玩「从断点号位精确续接原时间线」由此成立。 +- **EffectEngine 无状态化干净**:四词表封闭+加载期校验+计数器四桶全经 EffectContext 引用束由宿主持有,dispatch 内 round_start 整桶就地 clear 而非重赋值断引用——架构原则 8 的教科书式落地。 +- **DamageMath 零状态纯算术+DamageController 单据管线**(双结账政策开关、A/B 模式同管线)——「何时开单归编排、怎么算归控制器」切分清晰,伤害序(主角先手→逐敌还手→地图账殿后)有显式承载。 +- **save_manager 工程质量高**:tmp+rename 原子写、v1→v7 迁移链自注册、损坏兜底改名留痕、高版本拒读禁写、三态回滚的槽激活原语、值拷贝进出户纪律(注:_rename_overwrite 先删后改在 Windows 上有极小的双失窗,已注释自知,出包前可换引擎级原子替换)。 +- **LAYER_CONTRACT 层序单口、RunScope 会话生命周期、ModalFlow 纯逻辑可单测**——呈现层装配是全库最成熟的部分;run_scope 23 行纯状态与架构文档逐句一致。 +- **views 层工程惯例全面铺开**:_process 早退守卫、tween 先 kill 后建、is_instance_valid 守卫、探针只读快照模式(duplicate 防外泄)、无深节点路径(全代码构建 UI)。 +- **schema 类模式统一**:Resource+@export 带值域注释+validate() 错误数组,19 个 schema 零例外。 + +--- + +## 五、结论与后续 + +架构设计本身没有需要推翻的东西,模式选型与领域匹配;当前债务集中在三步可收敛:**闸机补洞(S9 正则修复+S6/S7/S8 落地)→ 规范对齐(§1.3/模块归属/行类型词表一次对齐批)→ 行为缺陷定点修(C6-C10+定点清单)**。已按四条登记待办清单:game 行为缺陷与结构重构候选(工程直入·待写卡)/S9 正则修复(工程直入·待写卡)/S6/S7/S8 落地(工程直入·待写卡)/架构与规范对齐批(文档轨·待文档设计)。结构重构候选(§三 大文件表)非缺陷,是否立项由写卡段呈报用户裁决。 diff --git a/reports/需求卡片-S9类型闸正则修复批-2026-10-24.md b/reports/需求卡片-S9类型闸正则修复批-2026-10-24.md new file mode 100644 index 00000000..b01832b2 --- /dev/null +++ b/reports/需求卡片-S9类型闸正则修复批-2026-10-24.md @@ -0,0 +1,177 @@ +# 需求卡片:S9 类型闸正则修复批(R2 恒零命中+static 漏网+跨行签名盲区·存量归零) + +- 版本:**v1.2**(2026-10-24 v1.0 成文→2026-10-25 grilling 第一轮修订 v1.1〔两阻断+三应修折入〕→第二轮复核**通过**(B-1 定界链经独立核实成立:iter36 禁令文+gdstyle 禁令后在库先例 01774323+登记口径三证据齐),一条建议级修正折入定稿;复核结论见 §12) +- 来源:[待办清单](../待办清单.md)「S9 类型闸正则失灵修复——R2 恒零命中+static 函数漏网」(2026-10-24 登记,来源=审计发现,设计面=工程直入,状态=待写卡+已写卡)(唯一事实源:工程直入证据指认=审计报告 [reports/审计-game代码结构审查-2026-10-24.md](审计-game代码结构审查-2026-10-24.md) §二 C1〔已独立复现〕+本写卡会话逐处实查〔本卡 §3〕;规约依据=[test-conventions](../design/engineering/test-conventions.md) §8.4〔一切改动的撤销证明义务〕+2026-10-24 用户全项目 P0 禁令〔变异类测试全项目禁用、闸件改动无豁免——见 §4 契约 3 与 D4〕) +- 归属:`tools/audit_type_hints.py`(正则修复)+ `game/core/`、`game/presentation/` 存量注解/豁免补齐(闸件批,**非迭代批**——gdstyle 批同款执行形态) +- 排期:非迭代批不占迭代号,独立 worktree 批;估时 ≈0.75–1 工作日 +- 开工门:U0(用户发令)+ grilling 复核门过(§12);与卡①先后皆可(§0) + +## 0. 前置依赖与并行性 + +- **第〇步核对(2026-10-24 写卡会话)**:来源条目设计面=工程直入、状态=待写卡,条目现状带行号级证据(审计 §二 C1,正则两洞经 python 逐条对拍独立复现)→ **放行写卡**。 +- **与卡①(game 行为缺陷定点修批,同批写卡)**:零 hunk 交叠(grilling 实查确认:main.gd:565 vs :712/:1486、level_runner:632 vs :779/:895、unit_container:275 vs :55/:257/:270 皆同文件不同 hunk);卡① d18 已替本批消掉 `board_view.gd:998` 一件存量红——本批 T0 存量清点以**本批开工时盘面**为准(两卡先后皆可;若本批先行,卡①新增代码须全类型,其卡 §0 已注记)。 +- **与 gdstyle S13(2026-10-24 已落地,commit 01774323/8cdeb180)**:gdstyle 配置只开 quality/*、format/naming/order 全关——与 S9 类型面零重叠(其条目预告口径已兑现);本批产品码注解改动落在 S13 扫描面,禁产生基线外新违规。 +- **改动域**:本批混触 `tools/`(文档域路径)与 `game/`(代码域路径)——批末闸档按 `tools/run_all_gates.sh` 自身域划分判定执行(域划分唯一出处=脚本头注 :10-15;卡内不另列清单);闸件自身改动的质量义务=撤销证明(§4 契约 3/§7 T2;2026-10-24 用户全项目禁令,闸件改动无豁免)。 + +## 1. 目标与完成定义 + +**目标**:修复 `audit_type_hints.py` 的 `FUNC_DECL` 识别面三个洞,使 R2/R3 对「无返回类型带 `:` 尾的声明」「`static ` 前缀函数」「参数表折行的跨行签名」恢复命中能力;随后把浮出的全部存量红清零(补类型注解优先、`# type-hint: ignore` 带理由豁免兜底),闸退出码归 0;闸改动配撤销证明(test-conventions §8.4)。 + +**改动单元清单**(钉值表,🔴 契约 1): + +| # | 位置 | 现状(实查) | 改法 | +|---|---|---|---| +| g1 | `tools/audit_type_hints.py:25` `FUNC_DECL` | 三洞:①正则要求 `)` 后只跟 `->`/注释/行尾——无返回类型声明按 GDScript 语法必然以 `:` 结尾,**R2 目标形态 100% 失配**(R2 上线起恒零命中恒绿);②`^(\s*)func` 不认 `static ` 前缀——static 函数 R2/R3 双漏;③**逐行匹配不认参数表折行**(`\((.*)\)` 不能跨行)——首行无闭括号的跨行签名永不进入扫描面(grilling 静态证明:实证址 hud_presenter 不修此洞则永不浮出) | 三洞修复:接受 `:` 尾形态(返回类型组与 `:` 尾并存);声明头认 `static ` 前缀;**续行拼接预处理**(声明行括号不平衡时并入续行直至平衡再匹配——写卡会话发现的第三洞,超出审计 C1 两洞原文,§11 呈报)。规则语义(R1/R2/R3 判据、`_init` 豁免、`# type-hint: ignore` 行级豁免)零变化 | +| g2 | 存量红清零(浮出即清) | 审计实证存量(闸当前退出码 0 全绿):`game/core/level_runner.gd:632`(`current_battle()` 无 `->`)、`game/core/attunement.gd:10`(`static func slots(db, save)`)、`game/presentation/main.gd:565`(`build_adventure_params` 两参)、`game/presentation/hud_presenter.gd:12-13`(refresh 跨行签名内 untyped 参——**行号订正**:审计与 v1.0 写 :10-11 实为注释行)、`game/core/reward_economy.gd:104/119/131/146`(query_identity/query_full_snapshot 等无 `->`)、`game/core/unit_container.gd:275`(`default_target()`);本会话补证 `game/presentation/tune_sim_controller.gd:24`(`func _autoplay():` 无 `->` 返回值) | T0 修复后全量清点红清单(实证址为已知下限,非全集)→ 逐处补返回类型/参数类型注解;确不适型处 `# type-hint: ignore`+行内理由;清零后 `python tools/audit_type_hints.py` 退出码 0 | + +**完成定义**(全部满足才算本批完成): + +1. g1 三洞修复落地,规则语义零变化(R1 用独立正则 R1A/R1B 不受影响——T0 记改前 R1 读数、T2 对拍同值); +2. g2 存量红全量清点并归零:退出码 0、逐处红清单与处置(注解/豁免+理由)成表落实现报告; +3. 撤销证明记录齐(§7 T2——闸件改动的拦截钉面自证);撤销后仍绿=断言没钉在这个改动上,判缺漏; +4. 批末按 `run_all_gates.sh` 域口径选档全绿(S9 段 PASS 归零;S13 基线零新增)+改前改后读数对拍;产出物写齐(T3)。 + +## 2. 设计事实源 + +| # | 出处 | 取用条款 | +|---|---|---| +| D1 | 工程直入证据指认=审计报告 §二 C1(正则行为经 python 逐条对拍独立复现;实证存量清单)+本写卡会话实查(§3) | 两洞机理与存量下限 | +| D2 | [test-conventions](../design/engineering/test-conventions.md) §8.4 | 闸机判据改写必须配撤销证明钉住改动:只撤销该判据改动→最窄闸见红→精确还原→复绿记录——**gdstyle 闸件批在库先例**(commit 01774323「重复 load 红令→还原绿」同款形态) | +| D3 | [architecture](../design/engineering/architecture/architecture.md) 测试与质量域节 S9 行+static_scan 头注 | S9 载体=`tools/audit_type_hints.py`(R1/R2/R3 判据文字=现行规则语义,本批零变化);「规则扫描面为空=恒绿」静默失效模式自警在案 | +| D4 | 2026-10-24 用户全项目 P0 禁令(变异类测试全项目禁用,**闸件改动无豁免**;一切验收只走撤销证明) | 本卡一切验证面(含闸件判据改动)一律撤销证明:只撤销该改动→最窄闸见红→`git checkout` 精确还原→复绿记录;撤销后仍绿=判据没钉在改动上,判缺漏。旧「闸件豁免」定界句作废 | +| D5 | [code-conventions](../design/engineering/code-conventions.md) §4(语言纪律——类型闸为其机器闸) | 注解补齐的产品面纪律依据 | + +## 3. 现状事实(2026-10-24 写卡会话实查+2026-10-25 grilling 独立补查;开工日 T0 复核) + +| # | 事实 | 出处 | +|---|---|---| +| G1 | `FUNC_DECL = re.compile(r"^(\s*)func\s+(_?\w+)\s*\((.*)\)\s*(->\s*.+)?\s*(#.*)?$")`——①无返回类型声明(`func f():` 形态)不匹配(`:` 不在允许尾集)→ R2 体扫描对该形态永不启动;②`static func` 头不匹配 → static 函数 R2/R3 双漏;③逐行匹配,参数表折行(首行无闭括号)永不匹配——**grilling 静态证明**:实证址 hud_presenter.gd:12-13(static 前缀+折行双形态)不修第三洞则永不浮出 | `tools/audit_type_hints.py:25`;`game/presentation/hud_presenter.gd:12-13` | +| G2 | R2 判据链:`FUNC_DECL.match` → 无 ret 且非 `_init` → 收集更深缩进连续体 → `RETURN_VALUE` 命中即红;R3:参数按 `PARAM_SPLIT` 拆分逐参查 `:`;`_init` 参数豁免;行内 `# type-hint: ignore` 整行跳过(:43-44);R1 用独立正则(R1A/R1B)与 FUNC_DECL 无关——带 `->` 的普通函数声明现行确在命中面(`->\s*.+` 贪婪吞尾) | `tools/audit_type_hints.py:22-27/42-74` | +| G3 | 实证存量(审计列+本会话与 grilling 双重抽核形态一致):`level_runner.gd:632`/`attunement.gd:10`/`main.gd:565`(static 洞);`reward_economy.gd:104/119/131/146`/`unit_container.gd:275`(`:` 尾洞);`hud_presenter.gd:12-13`(static+折行——第三洞唯一实证址);`tune_sim_controller.gd:24`(`:` 尾洞,本会话补证)。grilling 独立判定:两洞修复后除 hud_presenter 外全部浮出,hud_presenter 须第三洞 | 上列 file:line | +| G4 | 既有豁免先例:`board_generator.gd:281/331` 行内 `# type-hint: ignore(同 generate——测量面 stub 注入)`——豁免带理由的格式先例 | `game/core/board_generator.gd:281/331` | +| G5 | 闸机承载:static_scan.sh S9 段直跑 `python tools/audit_type_hints.py`(fail 聚合);闸头注自述三条规则与白名单——g1 修复后头注规则语义描述不变(实现到位非规则增删)。**S13 gdstyle 段已在役**(static_scan.sh 新增段,01774323)——本批产品码注解改动须 S13 基线零新增 | `game/tests/static_scan.sh` S9/S13 段;`tools/audit_type_hints.py:3-14` | +| G6 | 闸现状=退出码 0 全绿(R1/R2/R3 现行命中皆零——「R1/R3 仍有命中能力」指判据可达非在册命中,G2);audit_type_hints.py 内无逐规则零命中自检(static_scan 的 require_scan_face 只查扫描面文件数)——本批 g1 修复后,红清单浮出本身即扫描面非空的实证 | `python tools/audit_type_hints.py` 现状读数;`game/tests/static_scan.sh:27-36` | + +## 4. 设计契约(红线,违反 = L3 停机呈报) + +🔴 **契约 1 · 规则语义零变化**:g1 只修识别面到位——R1/R2/R3 判据文字、`_init` 豁免、`# type-hint: ignore` 白名单、扫描面(game/core+game/data+game/presentation)、退出码协议全部不变;第三洞(续行拼接)是实现面扩展非规则增删(§11 呈报);新洞再发现走 L3 呈报,禁批内自裁扩面。 + +🔴 **契约 2 · 存量清零双通道纪律**:补类型注解优先(产品码可注解处一律注解——duck-typed 注入面如 `db_ref`/`panel_ref` 可注真实类型或以 `# type-hint: ignore(理由)` 豁免);每处豁免行内带理由(G4 格式);禁用「删参数/改签名」替代注解(那是行为改动,越本批权——本批产品码面**零行为差**,与 P0 令的定界依据)。 + +🔴 **契约 3 · 撤销证明义务(闸件改动)**:g1 为改写闸机判据——撤销证明记录齐(§7 T2,gdstyle 批同款形态),撤销后仍绿=「断言未钉在改动上」判缺漏,不得写「已验收」;**回退实验只在闸件与本批注解面上做**(回退该判据改动→闸红→还原),禁在生产行为面上做。 + +🔴 **契约 4 · 实证址全覆盖**:T1 清点后 G3 所列实证址(含 hud_presenter:12-13 与 tune_sim_controller:24)必须全部出现在红清单或已注解命中——任一实证址未浮出即 L3 停机呈报(第三洞拼接实现有缺陷的第一信号),禁静默放过。 + +## 5. 范围边界 + +**做**:g1 三洞+g2 存量清零+撤销证明+产出物。 + +**不做**(明示去向): +- 第四类新洞(如 lambda 函数体、多行返回类型)——本批三洞为审计 C1 两洞+写卡会话静态证明必需的第三洞;再有发现走 L3 呈报; +- S6/S7/S8 落地——条目③整条冻结待④(2026-10-24 用户裁); +- gdstyle 引入——已另行落地(S13,8cdeb180),本批零触碰其配置与基线(仅产品码改动遵守基线零新增); +- 顺带改任何产品码行为(注解与豁免注释除外——GDScript 类型注解零运行时行为差)。 + +## 6. 关联文档同步面 + +> 本批是闸件批(非迭代批),可写面=`tools/`+`game/` 注解+`reports/`;`design/` 与根规则文件零触碰。 + +| 文档 | 关联点 | 同步义务 | +|---|---|---| +| [architecture](../design/engineering/architecture/architecture.md) 测试与质量域节 S9 行 | 「S9 在位」陈述不变(修复是实现到位非规则增删)——写卡会话核对该节无「正则形态」级陈述 | 无关联文档(查讫) | +| [test-conventions](../design/engineering/test-conventions.md) §6(旧条款) | 规则文本修正已由 2026-10-24 文档批完成——§6 退役、验收一律撤销证明(§8.4),闸件改动无豁免 | 已销号(随禁令落地,无需再裁) | +| `game/tests/README.md` 工具速查表 audit_type_hints 行 | 描述为规则语义级则零改动(T0 核) | 无关联文档(预期查讫,T0 复核) | +| `tools/audit_type_hints.py` 头注 | 规则语义未变零改动;若头注含正则形态级描述则随 g1 订正(T0 核) | 实施本体(可写) | + +## 7. 任务卡 + +> 估时按 Claude 工作日口径;回归措辞:里程碑步出口跑一次全套闸;波内修复只做最窄确认。 + +### 卡前 · 文件结构图 + +| 文件 | 职责 | 行为改动 | +|---|---|---| +| `tools/audit_type_hints.py` | g1 三洞修复(复用既有·修改) | 闸件改动 → wg1 | +| `game/core/*.gd`、`game/presentation/*.gd` 存量位点 | g2 注解/豁免补齐(复用既有·修改;位点全集=T1 清点) | 非行为改动(注解面)→ wg2 | +| `reports/实现报告-S9类型闸正则修复批-<完成日>.md` 等 | 产出物落位 | 非行为改动 → wg3 | + +### 接口块 + +- g1 修复后 `FUNC_DECL` 行为规格(断言与撤销证明引用以此为准):①`func f():`(无返回类型 `:` 尾)进入 R2/R3 扫描面;②`static func f(...)` 同普通函数进入;③参数表折行(首行 `(` 未闭合)经续行拼接后进入——hud_presenter:12-13 形态必浮出;④既有匹配面不回退(带 `->` 的普通函数声明改后仍匹配——T2③ 对拍)。 +- 最窄闸=`python tools/audit_type_hints.py`(直接退出码判读);全套闸=`tools/run_all_gates.sh` 按域划分选档(混触 `tools/`+`game/`)。 + +### 落位与可复用性预检 + +- **g1 改 `FUNC_DECL` 与预拼一段**——复用面:闸内单点常量与逐行循环,无第二处正则(G2);不新增文件不新增函数(续行拼接并入既有行循环)。唯一面:函数声明识别的唯一入口,修复即全局生效。分层:纯工具件零产品码依赖。 +- **g2 注解补齐**——零新增文件零新增函数;豁免格式复用 G4 先例;为什么不另立白名单文件:现行行级豁免通道即成文白名单,增第二通道=双源。 + +### T0 事实坐实+改前读数(≈0.25d) + +- [ ] 复核 §3 全部位点+头注/README 是否含正则形态级描述+跑改前读数(退出码 0、R1/R3 命中数对拍基线——G6 预期皆零)。可核查结果:改前读数表在案。 + +### T1 正则修复+存量清点归零(≈0.25–0.5d) + +- [ ] 实装 g1 三洞。可核查结果:临时注入探针(任选产品码文件加 `func _probe_r2():`+`return 1`、`static func _probe_r3(a):`+`return a`、跨行签名探针 `func _probe_ml(x,\n\ty):`+体)→ `python tools/audit_type_hints.py` 输出含三探针红(探针随后删除——探针件随写随删,不留在库)。 +- [ ] 全量清点红清单(G3 实证址齐全性核对——契约 4,hud_presenter:12-13 与 tune_sim_controller:24 必须浮出)→ 逐处注解/豁免 → 退出码 0。可核查结果:红清单处置表(file:line+处置+豁免理由)落实现报告。 + +### T2 撤销证明(≈0.25d) + +- [ ] 四步撤销证明(test-conventions §8.4;最窄闸直跑;实验面=闸件+本批注解): + ①**`:` 尾修复半**:只回退 g1 的 `:` 尾修复半 → `_probe_r2` 探针不再命中(红清单消失)→ 还原——证明 R2 命中钉在该修复半上; + ②**static 修复半**:单独回退 `static ` 前缀半 → `_probe_r3` 探针不再命中 → 还原; + ③**既有面回归**:带 `->` 普通函数声明的匹配形态改前改后对拍同命中(防修复吞旧面)+R1 读数改前改后同值(R1 独立正则不受影响的实证)——**「改前」载体写死**=临时回退 g1 全量复现改前读数再还原; + ④**真实存量红**:T1 红清单任取一址临时去掉新注解 → 该址复红 → 还原。 + 可核查结果:撤销点(文件:行)、闸输出摘要、还原结论四行记录表进实现报告。 +- [ ] 验收步骤:全套闸按域划分选档跑一次,期望全绿(S9 段 PASS 归零读数+S13 基线零新增)。 + +### T3 产出物落位卡(≈0.25d,末卡) + +- [ ] 写齐产出物:`reports/实现报告-S9类型闸正则修复批-<完成日>.md`(两单元状态+红清单处置表+撤销证明记录表+改前改后读数对拍)+`reports/代拍板追认单-S9类型闸正则修复批-<完成日>.md`(§10 白名单内代拍项留痕)+`reports/反哺点-S9类型闸正则修复批-<完成日>.md`(§6 各条落点——查讫项写「无」)。可核查结果:三文件在 `reports/` 且逐条有落点。 +- [ ] 合并前置:**独立验收审计**(test-conventions §8.7——未参与实现的会话/子代理;只读本卡、实现报告、diff 与闸件,自跑最窄抽查不采信「已全绿」)完成且报告 `reports/S9类型闸正则修复批-验收审计-<完成日>.md` 逐行「通过」——审计面=撤销证明记录表四步齐、实证址覆盖核对齐(含 hud_presenter/tune_sim 两补证址)、退出码 0 读数在案,缺任一项不通过;未通过不得合并。 + +**估时合计 ≈0.75–1d**。切分预案:无超软盒预挂面(实证址未浮出→L3 呈报即停,不吞工时)。 + +## 8. 验证闸 + +### 改动—验收对照表 + +| 需求点 | 改动 | 验收测试(名称+落点) | 层级 | 真实入口 | 撤销证明 | +|---|---|---|---|---|---| +| wg1 `:` 尾洞 | `audit_type_hints.py:25` | 探针 `_probe_r2` 命中〔wg1〕T1 | 闸机 | 闸真实跑批路径(static_scan S9 段同命令) | T2① 单独回退 → 探针漏网 | +| wg1 static 洞 | 同行 `static ` 前缀 | 探针 `_probe_r3` 命中〔wg1〕T1 | 闸机 | 同上 | T2② 单独回退 → 探针漏网 | +| wg1 跨行洞 | 续行拼接预处 | 跨行探针 `_probe_ml` 命中+hud_presenter:12-13 浮出〔wg1-ml〕T1 | 闸机 | 同上 | 回退拼接段 → 跨行探针与 hud_presenter 址双双漏网 | +| wg1 回归线 | 既有匹配面 | `->` 形态对拍+R1 读数同值〔wg1-reg〕T2③ | 闸机 | 同上 | 对拍差异即红 | +| wg2 存量归零 | `game/` 各存量位点注解/豁免 | `python tools/audit_type_hints.py` 退出码 0〔wg2〕 | 闸机 | 同上 | T2④ 去注解 → 复红 | +| wg3 产出物 | T3 三份 | 件核对〔wg3〕 | — | — | 缺项红 | + +**闸口径**:批末全套闸按 `run_all_gates.sh` 域划分选档(混触 `tools/`+`game/`,域划分唯一出处=脚本头注);S9 段归零 PASS 为本批主判据。**测试削弱申报**:零(闸判据只收紧到位、未放宽任何规则;产品码改动全部为注解/注释零行为差)。 + +## 9. 异常分级 + +- **L1(自救)**:注解 vs 豁免的逐址取舍;探针注入的临时落位选择;续行拼接的实现细节(括号计数 vs 续行缩进判)。 +- **L2(记录继续)**:存量清单超出实证址的量级波动(正负皆记)。 +- **L3(停机呈报)**:任一实证址未浮出(拼接实现缺陷第一信号);修复导致既有匹配面回退(T2③ 对拍红);正则修复需触碰 R1 正则(超钉值表);第四类新洞出现。 + +## 10. 代拍板白名单(超清单必呈报) + +1. 逐址注解 or 豁免的取舍(豁免必须带行内理由)。 +2. 探针注入的临时文件与清理时机。 +3. 续行拼接的实现写法。 +4. 提交切分与 worktree 分支命名。 + +## 11. 待裁点 + +**零待裁点,开工门=U0。** + +三处灰色地带已推导定值(用户可否决,随 T3 反哺点呈报): +- **第三洞(续行拼接)扩收**——超出审计 C1 两洞原文;依据=hud_presenter 实证址不修则永不浮出、契约 4 九址全覆盖必然停机(grilling 静态证明);备选「该址单行重排」淘汰依据=格式迁就闸而非闸达意;备选「移出实证址按未覆盖呈报」淘汰依据=明知的盲区留闸=再养恒绿面。 +- **闸件改动同样只走撤销证明(2026-10-24 用户全项目禁令,无豁免)**——旧「闸件豁免」定界作废;行为与闸件改动的验收口径统一为撤销证明(test-conventions §8.4),旧 §6 条款已随禁令退役。 +- **存量清零以注解优先、豁免兜底**——依据=code-conventions §4 类型纪律本意;备选「全豁免快速归零」淘汰依据=闸修复后第一轮即全豁免=把 S9 再养回恒绿摆设。 + +## 12. 自审六查与 grilling 复核记录 + +**自审六查(2026-10-24 写卡会话)**:①规格覆盖——g1/g2 挂 T0–T3、指到 wg1–wg3;②步骤扫描——一步一动作一可核查结果;③类型一致性——探针形态与 G1/G2 正则行为逐字对齐;④审查焦点+改动覆盖——补挂:回归线对拍(T2③)、实证址全覆盖契约(契约 4,含两补证址)、豁免滥用防线(§11);⑤篇幅比例——两单元小卡不膨体;⑥落位与复用——零新增文件,豁免复用 G4 先例、白名单单通道。 + +**grilling 第一轮复核记录(2026-10-25,独立子代理对抗复核)**:判回炉两阻断——B-1 验证面与 2026-10-24 P0 令冲突未呈裁;B-2 hud_presenter 跨行签名静态证明永不浮出、契约 4「九址全覆盖」=预排必然停机。随同三应修(B-3 G6「R1 存量命中在役」失实——闸现状全绿零命中;B-4 hud_presenter 行号 :10-11→:12-13;B-5 tune_sim_controller:24 升实证)+三建议(含域档引用脚本行号)。v1.1 折入:B-1 曾按当时在库证据拟定「闸件豁免」定界(gdstyle 先例 01774323);**该定界后经用户 2026-10-24 全项目禁令推翻**——闸件改动无豁免,卡面验证义务已全量改写为撤销证明(D4/契约 3/T2 现行口径);B-2 扩第三洞续行拼接(g1/契约 4/T1 探针/T2 对应全链落位,§11 呈报超出 C1 原文);G3/G6 事实按 grilling 订正;域档引用钉脚本头注 :10-15。复核结论:两阻断闭合、三应修落位(v1.1)。 + +**grilling 第二轮复核记录(2026-10-25,同代理续核)**:总判**通过**——B-1 定界链经独立核实成立(①iter36 纪律变更段原文全域禁令无闸件豁免=冲突真实;②commit 01774323 系 gdstyle 闸件批在库先例(同形态回退实验);③gdstyle/S9 两条目登记原文同款口径——复核时点证据齐,B-1 当时撤回;**其定界结论后经用户 2026-10-24 全项目禁令最终裁定推翻**(禁令在后、口径以此为准);B-2 第三洞全链落位(探针/契约 4/撤销证明联动)。余两条建议级:T2③ 改前载体写死(v1.2 已折入)、续行拼接后 R2 体收集含参数续行但无 return 故无害(实现期知晓项)。零阻断零应修,卡进可编排态。 diff --git a/reports/需求卡片-game行为缺陷定点修批-2026-10-24.md b/reports/需求卡片-game行为缺陷定点修批-2026-10-24.md new file mode 100644 index 00000000..a4bd94a9 --- /dev/null +++ b/reports/需求卡片-game行为缺陷定点修批-2026-10-24.md @@ -0,0 +1,313 @@ +# 需求卡片:game 行为缺陷定点修批(代码结构审查 §二 行为缺陷+定点小件) + +- 版本:**v1.2**(2026-10-24 v1.0 成文→2026-10-25 grilling 第一轮修订 v1.1〔三阻断+九应修折入〕→第二轮复核**通过**,四条建议级修正折入定稿;复核结论见 §12) +- 来源:[待办清单](../待办清单.md)「game 代码结构审查批——行为缺陷定点修+结构重构候选」(2026-10-24 登记,来源=审计发现,设计面=工程直入,状态=待写卡+已写卡)(唯一事实源:工程直入证据指认=审计报告 [reports/审计-game代码结构审查-2026-10-24.md](审计-game代码结构审查-2026-10-24.md) §二+本写卡会话逐处 file:line 实查〔本卡 §3,含对审计三处读数的订正〕;规约依据=[code-conventions](../design/engineering/code-conventions.md) §2.1/§4+[test-conventions](../design/engineering/test-conventions.md) §9.4+2026-10-24 用户 P0 令〔行为改动验收只走撤销证明,禁注入造红实验〕) +- 归属:`game/core/`(damage_controller / minesweeper_rules / level_runner / board_generator / action_economy / unit_container / status_container / treasure_items)+ `game/presentation/`(main.gd / debug_overlay / tune_sim_controller / modal_stack / views 下 standee_body / battle_end_overlay / battle_stage_view / settlement_fx_view / board_view / ui_kit)+ `game/data/`(shop 表与 hero 表扩列)+ 断言面(四件既有单测扩展+一件新单测+system_test 两断言+run_all 清单行) +- 排期:未编入(卡片不预占迭代号;本卡估时 ≈3–3.5 工作日,低于编排硬下限 5 工作日,须与其他卡同批编入——归 iter-plan 段四查③) +- 开工门:U0(用户发令)+ grilling 复核门过(§12)+前置:本卡先于 R1/R2/R3 重构批落地(§0) + +## 0. 前置依赖与并行性 + +- **第〇步核对(2026-10-24 写卡会话)**:来源条目设计面=工程直入、状态=待写卡,条目现状带行号级证据(审计报告 §二+本会话逐处实查复核)→ **放行写卡**。 +- **条目拆分(2026-10-24 用户裁)**:条目原含「结构重构候选(报告 §三)」——五件已裁**全部另立**为 R1(main.gd 判离)/R2(board_view 拆件)/R3(runner 拆件+断点 schema 单点化)三卡,本卡缺陷修**先行落地**后重构再动。缺陷九件中的**⑦明细行枚举漂移划归条目④**(「架构与规范对齐批」,词表归 13 还是 15 是④的裁点,本卡不自答、不碰 `battle_orchestrator.gd:15-19` 头注与架构文档 B1→D1 行)——本卡实收 = 八件缺陷+定点小件七件。 +- **与卡②(S9 类型闸正则修复批,同批写卡)**:两批零 hunk 交叠(卡②改正则+补注解,本卡改缺陷点;grilling 实查确认:main.gd:565 vs :712/:1486、level_runner:632 vs :779/:895、unit_container:275 vs :55/:257/:270 皆同文件不同 hunk),先后皆可;本卡 d18 给 `board_view.gd:998` 的 `lumas_out` 补类型=替卡②消掉一件存量红,卡② T0 存量清点以其落位时盘面为准。本卡**新增代码全类型**(接口块签名已钉全类型——含返回类型)。 +- **与迭代 36(已于 2026-10-24 实现入库,commit 9468c70e 等)**:文案清扫批 u7–u9 触点 `board_view.gd:730/:778/:782` 在本卡两触点(:998 与信号载体行)带外、`battle_orchestrator.gd:797`(u13)与⑦头注 :15-19 不同 hunk、开场后缀批触点 :608-616 与本卡 :712/:1486 不同 hunk、状态面板瘦身卡触点 status_panel/main._status_data 与本卡零重叠——grilling 实查「同文件不同 hunk」成立;d10 信号载体 28 处含 `camp_overlay_view.gd`/`board_view.gd`(iter36 触过的文件),按**现行盘面**(iter36 已并入)T0 复核行号。 +- **与 gdstyle S13 闸(2026-10-24 已引入,8cdeb180)**:本批对产品码的改动落在 S13 扫描面(core/data/presentation 100 个 .gd)——改动禁产生基线外新违规(S13 差集判红)。 + +- **卡后换代注记(2026-10-25 补)**:迭代 35 卡②(5499eb9c)在本卡成文后合入——`level_runner.gd` 行号整体漂移(`_spend_discounted` :777→:764、`pool_price` :891→:878、relic 15.0 :895→:882,**字面量与散折单点确认仍在**)、`main.gd` :712→:710/::1486→:1484;缺陷身份与修法零变化,T0 复核按现行盘面重采行号。 +- **回放确定性对拍触面**:本批 diff 触 `game/data/tables/`(d11/d16 扩列)与 `game/core/damage_controller.gd`(d1)——11 号闸将判「触面、须用户批准发起」;本批无数值调整(表值=原字面量原样迁移)、MC 零跑,对拍是否发起归用户,闸以 NEED-APPROVAL 留痕不判红。 + +## 1. 目标与完成定义 + +**目标**:代码结构审查 §二 的八件行为缺陷(C6–C10+定点清单三件)+定点小件七件全部定点修复,每件带断言与撤销证明;数值字面量两处与英雄名一处迁入数据表(值原样);全库信号注释载体统一 `##` 且两条零契约核心信号补写成文。 + +**改动单元清单**(钉值表,🔴 契约 1): + +| # | 位置 | 现状(实查) | 改法 | 来源 | +|---|---|---|---|---| +| d1 | `game/core/damage_controller.gd:215` | `if t.source_unit == u:`——Godot 4 Dictionary `==` 是逐键内容比较。**生产不可达实证**(grilling 订正):`unit_container.gd:98` 每敌单位带自增 `handle_id`(:77/:114),任意两敌内容必异,真实 spawn 路径不会触发误作废;该行属**语义加固件**(引用同一性判定才是 `_on_unit_died` 的正确语义,且防御未来单位字典形态演化) | 判定改 `is_same(t.source_unit, u)`;定性=防御性语义加固(非行为修复——撤销不红属预期不适用,见 wd1 证明设计) | 审计 §二 C6(前提读数经本会话订正——审计所述「内容全同双敌」被 handle_id 排除) | +| d2 | `game/presentation/views/standee_body.gd:171-172/188` | `set_pending` 蓄力档落 `GOLD_INK`(:172),`_play_intent_motion` 却判 `GOLD_DEEP`(:188)——③蓄力缓推分支恒不可达死代码 | 判据改布尔:新增成员 `_intent_charge := false`,`pid in CHARGE_SKILL_IDS` 分支内置 true,`_play_intent_motion` 判 `_intent_charge` 分档;颜色比较判据删除 | 审计 §二 C7 | +| d3 | `game/presentation/views/ui_kit.gd:28` | `GOLD_BRIGHT` 注释宣称「GOLD_DEEP 语义岗〔standee:199 意图判据/board_view:35/level_map:237〕在役不动」——**三处引述全部失真**(grilling 实查:board_view:35 实为 `DEPTH_PX`、level_map:237 在 abandon 段、standee 意图判据即 d2 死代码,两文件 GOLD_DEEP/GOLD_BRIGHT 引用 grep 零命中) | 该括号引述整段删除(不留条件保留),意图蓄力墨改引 `GOLD_INK` | 审计 §二 C7 连带 | +| d4 | `game/presentation/views/battle_end_overlay.gd:281-287/156-161` | `_play_celebrate` 建 `set_loops()` 无限 tween,`close()` 只隐藏不 kill——每次胜利叠加一个永不停的 tween | 新增成员 `_celebrate_tw: Tween`;`_play_celebrate` 开头 kill 旧句柄再建新;`close()` kill+置空(全库先 kill 后建惯例=board_view:362-364 先例) | 审计 §二 C8 | +| d5 | `game/presentation/views/debug_overlay.gd:84` | `_dbg_btn.disabled = _ctx_locked`——调试钮不看 `_sim_running`(模拟钮 :101 看两态) | 调试钮 disabled 并入 `_sim_running`(`set_sim_running` 同步刷调试钮) | 审计 §二 C9 | +| d6 | `game/presentation/main.gd:712-717` | `_start_debug_levels` 守卫只查 `is_adventure or settlement != null`,漏批量模拟运行态;:717 `reset_sliders()` 会作废在跑测量的滑条读数 | 守卫补 `_tune_sim.is_running()` 即返(模拟运行期调试钮已由 d5 置灰,此处为信号绕过 disabled 时的第二道闸) | 审计 §二 C9 | +| d7 | `game/presentation/main.gd:1486-1491` | `_on_tune_simulate` 守卫只查 `settlement != null`,漏 `is_adventure` | 守卫补 `or run_scope.is_adventure` | 审计 §二 C9 | +| d8 | `game/presentation/tune_sim_controller.gd` | 无运行态查询面——`run()` :37 起 `set_sim_running(true)`、:62 收 `false`,main 侧无从互查 | 新增 `var _running := false`+`func is_running() -> bool`;`run()` 首行置 true、收尾置 false(run 无早退分支) | 审计 §二 C9 | +| d9 | `game/presentation/modal_stack.gd:21-38` | `open()` 对未登记层(`_list.find(m)`=-1)静默隐藏全部已开层;同层重复 open 时第二次 `hidden` 为空表(上方已隐)→ `_occluded[m]` 被空表覆盖,原被隐藏层 close 后永不恢复 | fail-fast+遮挡账**合并非覆盖**:`open()` 首行 find<0 即 `push_error` 且 return;重复 open 仍重扫上方可见层(V6 遮挡语义不变——modal_flow_test :40-48 在役断言依赖之),`_occluded[m]` 改**旧账∪新隐去重合并**(吞账缺陷修复);`close()` 对未登记层 `push_error` | 审计 §二 C10(修法经 grilling A-2 订正:幂等直返方案与在役断言冲突,改合并制) | +| d10 | `game/core/minesweeper_rules.gd:8-9`+全库 28 处 | 两条核心信号零契约成文(违 code-conventions §2.1);全库信号注释载体 28 处 `#` 非 `##`(不进编辑器 F1 帮助;grilling 复算 28/51 精确吻合) | :8/:9 按 §2.1 三要素(参数语义〔载荷键集按发射位并集:cell_revealed 四处 :129/:303/:310/:345、seal_judged :476〕/发送时点/消费方依赖)补 `##` 契约;全库信号声明行的 `#` 注释逐处换 `##`(注释内容逐字保留) | 审计 §二 定点清单① | +| d11 | `game/core/level_runner.gd:779/895`+`game/data/schemas/shop_table.gd`+`game/data/tables/shop.tres`+`game/core/config_db.gd` | relic 基价 `15.0`(:895)与散折 `0.8`(:779)是玩法数值字面量(自注 provisional)——违数值单一事实源 | ShopTable 新增两个 knob:`relic_base_price: float = 15.0`(域 [1.0,99.0])+`spend_discount: float = 0.8`(域 (0.0,1.0]),`validate()` 补两域检;ConfigDB 新增 `shop_relic_base_price() -> float`/`shop_spend_discount() -> float`(:337-341 同式);level_runner 两处改读表;过时注释随动订正 | 审计 §二 定点清单② | +| d12 | 产发端 `game/core/damage_controller.gd:168/370/400/504`+解析端**两口** `game/presentation/views/battle_stage_view.gd:308(on_step_row_fx——闪白/火花/击杀灰化)与 :342-346(enemy_standee_center——弹道/标出)**+`game/presentation/row_pacer.gd:110-115/135-143`+`game/presentation/views/standee_body.gd` | 站桩解析两口都以 display_name 子串匹配(`_card_by_name` 的 `contains`,消费点 :308 与 :343——grilling A-3 订正:审计点名的「弹道/闪白/击杀」三症分住两口,v1.0 漏了 :308)——同名敌或名字互为前缀错位 | 产发端四处行内/载荷内**增键** `"target_id": <敌 enemy_id>`("target" 文本值保留——日志行文不变;:168/:400 属 fx 载荷面,其键是否随 fx 行下发到 :308 解析口=T0 核类);standee_body 新增 `func enemy_id() -> String`;battle_stage_view 新增 `_card_by_id(enemy_id)`(相等匹配),`enemy_standee_center` 改签名(id 先、display_name 兜底)、`on_step_row_fx` 解析同改(id 先、名兜底);row_pacer 取 `row.get("target_id","")` 传入 | 审计 §二 定点清单③ | +| d13 | `game/core/treasure_items.gd:55-62` | 三段孤儿注释(持有数/持有件记录/丢弃)指向已删函数(连空壳都不在) | 三段孤儿注释删除 | 审计 §二 定点清单④ | +| d14 | `game/core/board_generator.gd:97/334/281` | 目标雷数公式两处手抄(:97 与 :334 同式);`legality(b, params, db)` 的 db 参数未用 | 公式单点化(`static func` 一处、两调用点改引);`legality` 去 db 参数(调用点两处:board_generator.gd:53+tests/unit/core/board_gen_reward_test.gd:145,同批改) | 审计 §二 定点清单⑤ | +| d15 | `game/core/action_economy.gd:191-192`(注释 :185 随动) | 蓄力层上限 `3` 硬编码两处字面量(`mini(3,…)`/`>= 3`);:185 注释「上限 3」同源 | 模块常量 `const CHARGE_LAYER_CAP := 3`(机制常量非表值——结构常量先例 STANDEE_FOOT_MARGIN 同型);两处+注释随动改引 | 审计 §二 定点清单⑥ | +| d16a | `game/core/unit_container.gd:55`+`game/data/schemas/hero_table.gd`+`game/data/tables/hero.tres`+`game/core/config_db.gd` | 英雄名「探险者」硬编码绕开 HeroTable(敌人名走 row.display_name) | HeroTable 新增 `display_name: String = "探险者"`(非空域检);ConfigDB 新增 `hero_display_name() -> String`;unit_container:55 改读表 | 审计 §二 定点清单⑦ | +| d16b | `game/core/unit_container.gd:257/270`+`game/core/status_container.gd:58-61/102/114` | Dictionary 取值 Variant 裸算术(`u.hp - n`/`u.hp += actual`/`inst.layers …`)漏包 int() | 逐处算术前显式 `int()` 包裹(写回路径保持既有语义) | 审计 §二 定点清单⑧ | +| d17 | `game/presentation/views/settlement_fx_view.gd:94-95` | `await get_tree().process_frame` 后无 `is_instance_valid(lab)` 守卫(全库守卫惯例唯一漏网处) | await 后补 `if not is_instance_valid(lab): return` | 审计 §二 定点清单⑨ | +| d18 | `game/presentation/views/board_view.gd:998` | `_normalize_floor_luma` 的 `lumas_out` 参数缺类型且无豁免注记(S9 R3 面存量;调用点三处:board_view:957 传 `Array[Variant]`、terrain_tile_sets_test:135、ui_contrast_probe:150 传 null) | 参数补类型 `lumas_out: Array[Variant] = null` | 审计 §二 定点清单⑩ | + +**完成定义**(全部满足才算本批完成): + +1. 钉值表 19 行落地(d16 拆 a/b 两行计);d11/d16a 表值与原字面量逐位一致(15.0/0.8/「探险者」),无数值调整、MC 零跑; +2. §8.2 对照表逐行断言绿;**行为改动单元撤销证明双证齐**(回退该单元 hunk → 对应断言红 → 还原复绿——只撤销本批 hunk,禁注入/改值造红);**等价类单元如实申报**(d1 语义加固/d11/d16a 原值迁移/d13–d16/d18 等价重构——撤销不红属预期,以等价断言与读数对拍承载;d17 无独立断言以惯例一致性走查申报——见 §8.2 分行); +3. d10 载体统一后,`grep -rnP "^\s*signal\s+\w+\(.*\)\s*#(?!#)"` 对 `game/` 全域零命中; +4. 批末按 `tools/run_all_gates.sh` 域口径选档(本批只碰 `game/`,域划分唯一出处=脚本头注 :10-15)全绿(含 S13 基线零新增)+改前改后读数对拍;三份 `reports/` 产出物写齐(T3)。 + +## 2. 设计事实源 + +| # | 出处 | 取用条款 | +|---|---|---| +| D1 | 工程直入证据指认=审计报告 §二(C6–C10+已核实定点清单)+本写卡会话逐处实查(§3 F1–F21,含对审计三处读数的订正:C6 可达性前提/d12 解析口单复/路径) | 19 个改动单元的 file:line 现状与成因定性 | +| D2 | [code-conventions](../design/engineering/code-conventions.md) §2.1 | 每个跨类信号:参数语义、发送时点、消费方依赖必须在发送方 `##` 文档注释成文(d10 依据) | +| D3 | [code-conventions](../design/engineering/code-conventions.md) §4+AGENTS.md 数值单一事实源红线 | 玩法数值只存 `data/tables/`(.tres)经 ConfigDB 单点读取(d11/d16 依据;d15 判机制常量的边界=结构常量先例 STANDEE_FOOT_MARGIN) | +| D4 | [architecture](../design/engineering/architecture/architecture.md) 架构原则 5(fail-fast) | d9 push_error 早退依据(「层必须先 register 方可 open/close」已写在 modal_stack 头注,机器强制缺口本批补) | +| D5 | [test-conventions](../design/engineering/test-conventions.md) §9.4+2026-10-24 用户 P0 令(iter36-实现报告 §纪律变更段在案) | 行为改动验收只走撤销证明(回退本批 hunk→断言红),禁注入/改值造红实验;新断言判别力随所属改动的撤销证明一并自证,证不了的一律记「拦截能力未证」 | + +## 3. 现状事实(2026-10-24 写卡会话实查+2026-10-25 grilling 独立补查;开工日 T0 复核行号) + +| # | 事实 | 出处 | +|---|---|---| +| F1 | `_on_unit_died` 遍历 `_queue` 按 `t.source_unit == u` 置 `cancelled`——Dictionary `==` 在 Godot 4 为逐键内容比较。**可达性(grilling 订正)**:`unit_container.gd:98` 敌单位行带 `"handle_id": handle`,handle 随实例自增(:77/:114)——任意两敌单位字典内容必异,真实 spawn 路径下 `==` 只对同一字典为真,误作废不可达;该行是语义隐患(判定语义应为引用同一性)而非现行行为缺陷 | `game/core/damage_controller.gd:210-216`;`game/core/unit_container.gd:77/98/114` | +| F2 | 蓄力档判定链:`set_pending(ptype, pid)` :154;`pid in CHARGE_SKILL_IDS`(:7 `["heavy"]`)时 `col = UiKit.GOLD_INK`(:172,Color8(116,80,11));`_play_intent_motion` :182-194 判 `if _intent_col == UiKit.GOLD_DEEP`(:188,Color8(176,122,20))——常量错配,③蓄力缓推+定格微顿+回落分支 :189-191 恒不可达 | `game/presentation/views/standee_body.gd:7/154/171-172/182-194` | +| F3 | `GOLD_BRIGHT` 注释含「GOLD_DEEP 语义岗〔standee:199 意图判据/board_view:35/level_map:237〕在役不动」——grilling 实查三处引述全失真(board_view:35=`DEPTH_PX`;level_map:237 在 abandon 确认段;两文件对 GOLD_DEEP/GOLD_BRIGHT 引用 grep 零命中) | `game/presentation/views/ui_kit.gd:28` | +| F4 | `_play_celebrate` :281-287 `_cele.create_tween()`+`set_loops()`(无限)无句柄持有;`close()` :156-161 只置 visible=false+`_clear_crest_bricks()`,零 kill;`open_victory` :131 每调必 `_play_celebrate()`。全库先 kill 惯例先例=board_view:362-364 | `game/presentation/views/battle_end_overlay.gd:116-139/156-161/281-287` | +| F5 | `set_context_locked` :82-85 只置 `_dbg_btn.disabled = _ctx_locked`;`_sync_sim_button` :100-103 模拟钮看 `_sim_running or _ctx_locked`——同屏两钮判据不对称;`is_running()` :71-72 已有(面板态) | `game/presentation/views/debug_overlay.gd:71-103` | +| F6 | `_start_debug_levels` :712-717 守卫 `run_scope.is_adventure or run_scope.settlement != null`,:717 `debug_overlay.reset_sliders()`;批量模拟(tune_sim_controller.run 逐帧 await :58)与调试对局并发=演出扭曲+测量作废(autoplay 自建局部 RngService=`game/sim/level_autoplay.gd:44`,无全局 RNG 竞态) | `game/presentation/main.gd:712-724` | +| F7 | `_on_tune_simulate` :1486-1491 守卫只 `run_scope.settlement != null`;`_tune_sim` 构造点 :393(无 driver 注入) | `game/presentation/main.gd:393/1486-1491` | +| F8 | `TuneSimController` 零运行态查询面:`run(n)` :30-62 唯一执行体(`panel.set_sim_running(true)` :37 ↔ `:62`);RefCounted,`_init(db_ref, panel_ref, base_hp: int, driver = null)` 有 driver 注入位 | `game/presentation/tune_sim_controller.gd:16-62` | +| F9 | `open()` :21-30:`idx := _list.find(m)` 为 -1 时 `range(idx+1, size)` 隐藏**全部**已开层后 `_occluded[m] = hidden`、`m.visible = true`;同层重复 open 时第二次 `hidden` 为空表(上方已隐)→ `_occluded[m]` 被空表覆盖,原被隐藏层 close 后永不恢复;`close()` :33-38 对未登记层无防护。**在役断言约束(grilling 查出)**:`modal_flow_test.gd:40-48` V6 序列 open(a)→open(map)→open(a) 断言 `not map.visible`——依赖「重复 open 仍重扫上方」,幂等直返方案会使该断言变红(d9 采合并制的依据) | `game/presentation/modal_stack.gd:21-38`;`game/tests/unit/presentation/modal_flow_test.gd:40-48` | +| F10 | `cell_revealed`(:8)与 `seal_judged`(:9)零注释;同文件 `round_end_board`(:10)带 `#` 注释;全库信号声明行携 `#`(非 `##`)注释 28 处/声明总数 51(本会话与 grilling 双重 `grep -P` 复算一致);发射位:cell_revealed ×4(:129/:303/:310/:345)、seal_judged ×1(:476 `{"cell": c, "hit": …}`) | `game/core/minesweeper_rules.gd:8-10`;全库 grep 实数 | +| F11 | 散折单点 `_spend_discounted` :777-780 `maxi(1, int(floor(float(price) * 0.8)))`;relic 基价 :895 `int(ceil(15.0 * pow(...)))`。ShopTable 标量 knob 先例:`price_a`/`price_b`(:13-14,域检 :101-104);`SHELF_KINDS` 封闭词表 :18 不含 relic(relic=池令牌非货架种);ConfigDB shop 访问器族 :304-341 | `game/core/level_runner.gd:769-780/888-900`;`game/data/schemas/shop_table.gd:13-18/101-104`;`game/core/config_db.gd:304-341` | +| F12 | 站桩解析**两口**(grilling 订正):`on_step_row_fx` :308 用 `_card_by_name(target)` 解析受击站桩(闪白 :311→`_schedule_arrival_fx` :318-325 火花/击杀灰化)+`enemy_standee_center` :342-346(row_pacer :110-115 结算标出、:135-143 跨区弹道消费);`_card_by_name` :332-338 以 `contains` 子串匹配。产发端四处携 display_name:damage_controller :168(attack_commit fx **载荷**)、:370(fx 载荷)、:400(kill hits 载荷)、:504(damage **行**);哨兵 `"hero"`(:470/:527、orchestrator:885)与 `"all_enemies"`(:519)不经站桩解析;单位字典带 `enemy_id`(unit_container:99 敌/:55 英雄 `"hero"`);standee 有 `display_name()`(:65)与 `_enemy_id`(:44/:89 装载),**无 enemy_id 访问器** | `game/presentation/views/battle_stage_view.gd:308-346`;`game/presentation/row_pacer.gd:106-150`;`game/core/damage_controller.gd:168/370/400/470/504/519/527`;`game/core/unit_container.gd:55/99`;`game/presentation/views/standee_body.gd:44/65/89` | +| F13 | treasure_items :55-58「持有数」注释段与 :61-62「持有件记录/丢弃」注释段之间零函数体(:66 起才是 `_usable_now`) | `game/core/treasure_items.gd:51-66` | +| F14 | 雷数公式 :97(生成期)与 :334(产物断言期)同式手抄;`legality(b, params, db)` :281-283 db 参数未用(行内带 `# type-hint: ignore` 豁免),调用点两处 | `game/core/board_generator.gd:53/97/281-283/334`;`game/tests/unit/core/board_gen_reward_test.gd:145` | +| F15 | `finish_round_stop` :188-196 内 `mini(3, _charge_layers + _last_stop_ap)`(:191)与 `_charge_layers >= 3`(:192);:185 注释含「上限 3」 | `game/core/action_economy.gd:185-196` | +| F16 | 英雄单位构造硬编码 `"display_name": "探险者"`;HeroTable 现仅三数值轴(零文案字段);敌名走 `row.display_name` | `game/core/unit_container.gd:55`;`game/data/schemas/hero_table.gd:8-10` | +| F17 | Variant 裸算术:unit_container :257(`u.hp - n`)、:270(`u.hp += actual`);status_container :58-61(`inst.layers` 比较/自增)、:102(`inst.layers * inst.snap_m`)、:114(`inst.remaining -= 1`) | `game/core/unit_container.gd:250-271`;`game/core/status_container.gd:56-64/100-119` | +| F18 | `play_damage_number` :94 `await get_tree().process_frame` 后 :95 直取 `lab.size`——lab 在等待帧间释放即错;全库 await 后守卫惯例(如 `_schedule_arrival_fx` :318-329) | `game/presentation/views/settlement_fx_view.gd:85-99` | +| F19 | `_normalize_floor_luma` static,`lumas_out = null` 无类型;调用点三处(board_view:957 传 `Array[Variant]`、terrain_tile_sets_test:135、ui_contrast_probe:150 传 null) | `game/presentation/views/board_view.gd:950-998`;`game/tests/unit/presentation/terrain_tile_sets_test.gd:135`;`game/probe/ui_contrast_probe.gd:150` | +| F20 | 断言宿主在役实况:damage_controller_test(直构 ConfigDB/RngService/UnitContainer/StatusContainer/DamageController,已覆盖「死亡作废」面);modal_flow_test(直构 ModalStack+哑 Control,:40-48 V6 序列在役);level_shop_economy_test(商店域);pacing_queue_test(row_pacer 排播域);system_test(真实玩法路径宿主);单测入闸协议=run_all.gd 套件清单显式登记行。**脚本错判定面**:单件直跑退出码不含运行期脚本错——判红须经 `tools/error_scan.py` 扫直跑输出(run_all.gd:136 同款机制) | `game/tests/unit/battle_settlement/damage_controller_test.gd:9-30`;`game/tests/unit/presentation/modal_flow_test.gd:1-48`;`game/tests/run_all.gd:91/:101/:136` | +| F21 | 改前读数基线=开工日 T0 采集(写卡会话不跑测——测试执行两入口铁规),本卡不预写通过数 | — | + +## 4. 设计契约(红线,违反 = L3 停机呈报) + +🔴 **契约 1 · 钉值表逐字**:§1 钉值表 19 行逐字落地;d11/d16a 为**原值迁移**(15.0/0.8/「探险者」),禁顺带调值禁增减字段语义;d2 是修成 [战斗表现](../design/gdd/systems/战斗循环/战斗表现.md) §4.6.4 已成文的三档运动签名(激活死代码=对齐设计);d12 行内/载荷内**只增键不改值**("target" 的 display_name 文本保留——日志行文零变化)。 + +🔴 **契约 2 · core 触碰面封闭清单**:core 文件改动全部为钉值表所列定点(damage_controller 一处判定符+四处增键、minesweeper 两行契约、level_runner 读表替换+注释订正、board_generator 公式单点+去参、action_economy 常量化+注释随动、unit_container 读表+int() 包裹、status_container int() 包裹、treasure_items 注释删)——**battle_orchestrator 零触碰**(⑦头注归条目④;orchestrator:718 treasure_used 行的 target 形态 T0 核类,若为敌 display_name 则同批增键,属 d12 产发端清单的 T0 扩面非新单元);零编排逻辑、零结算序、零随机流改动。 + +🔴 **契约 3 · 表扩列纪律**:ShopTable/HeroTable 新字段全部带 `validate()` 域检(F11/F16 既有条目同式);消费面一律经 ConfigDB 新访问器;跨表校验器(`config_db_cross_validator.gd`)是否需登记新字段=T0 核(其只校键集的域不涉新标量则零改动)。 + +🔴 **契约 4 · 验收闭合与证明纪律(2026-10-24 P0 令)**:行为改动单元(d2/d4/d5/d6/d7/d8/d9/d12/d10-契约半)撤销证明=**只回退本批该单元 hunk** → 最窄闸对应断言红 → 还原复绿;**禁注入造红、禁改值造红**(等价类单元 d1/d11/d13–d18 撤销不红属预期,如实申报、以等价断言与读数对拍承载);断言判别力证不了的面一律记「拦截能力未证」,禁硬凑。 + +🔴 **契约 5 · 断言面零未申报削弱**:既有断言零删除零放宽;**d9 修法与 modal_flow_test :40-48 V6 在役断言必须同绿**(合并制设计的硬约束);d14 改 board_gen_reward_test:145 调用签名属随签名同批改(非削弱,申报在案);新增断言全列 §8.2。 + +🔴 **契约 6 · golden 判定先于动作**:d2/d12 是行为修正,渲染闸棋盘三图可能 changed(蓄力敌局/同名敌局的意图与弹道演出变化=修对的证据而非回归);本批不预重制,按 [golden README](../game/tests/golden/README.md) 双态判定(判有效加注;判失效显式呈报后重制+复闸);菜单基线预期无涉。 + +🔴 **契约 7 · d10 注释逐字保留**:28 处信号注释只换载体 `#`→`##`,注释内容逐字不动(含旧词——用语清扫另有其批);minesweeper 两契约新写成文按 §2.1 三要素,载荷键集以发射位实查并集为准(T0 复核四处发射位键集后落笔)。 + +## 5. 范围边界 + +**做**:§1 钉值表 19 行;四件既有单测扩展+一件新单测+run_all 清单行+system_test 两断言;两表扩列与访问器;渲染闸 golden 判定动作;反哺点清单。 + +**不做**(明示去向): +- ⑦明细行枚举漂移(词表 13/15)——归条目④「架构与规范对齐批」裁决,④落文前 battle_orchestrator 头注不动; +- S9 正则修复与其余存量注解归零——归卡②(本卡仅 d18 一件顺带补类型); +- 结构重构五件——R1/R2/R3 三卡(本卡先行); +- 报告 §三「其余小件」(level_map_screen 视图直读与 BFS 重算、main.gd 死变量与 confirm 旁路、settle_batch 空判、row_pacer 常驻 flush、`_fatal` assert 剥离、rng_service open_stream early-return)——**未入本卡**(改善级非缺陷级、审计未标写卡面),留审计报告在册待用户后续点名或随重构批收编; +- 意图动画重设计、庆祝演出换装——零设计面(本卡只修可达性与句柄纪律)。 + +## 6. 关联文档同步面 + +> 本批是代码实现批,禁写 `design/` 整目录与根规则文件,唯一可写目录=`reports/`。同步义务一律执行为:逐条列进 `reports/iter-反哺点-<完成日>.md`,用户会话裁决后归文档批落文。 + +| 文档 | 关联点 | 同步义务 | +|---|---|---| +| [战斗结算](../design/gdd/systems/战斗循环/战斗结算.md) 明细行协议节 | d12 行内增 `target_id` 键=行协议字段集扩列(T0 查该节是否逐键登记行字段,列则补一行) | 反哺点 · 列清单(①增键说明+依据=本卡 d12;呈现消费语义不变) | +| [存档序列化](../design/gdd/systems/表现与支撑/存档序列化.md)/[行动经济](../design/gdd/systems/排雷循环/行动经济.md)/[战斗表现](../design/gdd/systems/战斗循环/战斗表现.md)/shop 相关 GDD | d11/d15/d16 值未变仅迁源、d2 对齐已成文设计——写卡会话检索(散折/基价/蓄力层/英雄名关键词)零陈述被本批改变 | 无关联文档(查讫) | +| `待办清单.md` | 来源条目批后按拆分结论退役/换代(归 batch-closure) | 反哺点 · 列清单(②批末退役笔) | +| `roadmap.md` | 迭代行编入笔(归 iter-plan 段发令) | 反哺点 · 列清单(③) | +| `game/data/tables/shop.tres`/`hero.tres`、`game/tests/`、探针与单测文件 | 实施本体与断言面 | 实施本体(可写) | + +## 7. 任务卡 + +> 估时按 Claude 工作日口径;每卡出口自跑轻闸;开工日 T0 复核本卡全部行号。回归措辞模板:里程碑步(T 段)出口跑一次全套闸;波内修复只做出红最小单元的最窄确认;新增测试先行孤立验证再并入全套。 + +### 卡前 · 文件结构图 + +| 文件 | 职责 | 行为改动 | +|---|---|---| +| `game/core/damage_controller.gd` | d1 判定符+d12 四处增键(复用既有·修改) | d1 非行为改动(语义加固)→ wd1;d12 行为改动 → wd12 | +| `game/presentation/views/standee_body.gd` | d2 `_intent_charge` 布尔分档+d12 `enemy_id()` 访问器(复用既有·修改) | 行为改动 → wd2/wd12 | +| `game/presentation/views/ui_kit.gd` | d3 注释订正一行(复用既有·修改) | 非行为改动(注释)→ 随 wd2 复核 | +| `game/presentation/views/battle_end_overlay.gd` | d4 tween 句柄 kill 纪律(复用既有·修改) | 行为改动 → wd4 | +| `game/presentation/views/debug_overlay.gd` | d5 调试钮判据并入模拟运行态(复用既有·修改) | 行为改动 → wd5 | +| `game/presentation/main.gd` | d6/d7 两守卫补口(复用既有·修改) | 行为改动 → wd6/wd7 | +| `game/presentation/tune_sim_controller.gd` | d8 `is_running()` 运行态查询面(复用既有·增成员) | 行为改动 → wd8 | +| `game/presentation/modal_stack.gd` | d9 fail-fast+遮挡账合并制(复用既有·修改) | 行为改动 → wd9 | +| `game/core/minesweeper_rules.gd`+全库 28 信号行 | d10 两契约成文+载体统一(复用既有·修改) | wd10(契约成文=行为改动·注释面;载体统一=非行为改动) | +| `game/data/schemas/shop_table.gd`+`game/data/tables/shop.tres`+`game/core/config_db.gd` | d11 两 knob+域检+访问器(复用既有·扩列) | 等价类(读数源迁移)→ wd11 | +| `game/core/level_runner.gd` | d11 两处读表+注释订正(复用既有·修改) | 等价类 → wd11 | +| `game/presentation/views/battle_stage_view.gd`+`game/presentation/row_pacer.gd` | d12 两口按 id 解析+target_id 取用(复用既有·修改) | 行为改动 → wd12 | +| `game/core/treasure_items.gd` | d13 孤儿注释删(复用既有·删注释) | 非行为改动 → wd13 | +| `game/core/board_generator.gd` | d14 公式单点+legality 去参(复用既有·修改) | 非行为改动(等价重构)→ wd14 | +| `game/core/action_economy.gd` | d15 常量化+注释随动(复用既有·修改) | 非行为改动 → wd15 | +| `game/data/schemas/hero_table.gd`+`game/data/tables/hero.tres`+`game/core/config_db.gd`+`game/core/unit_container.gd` | d16a 英雄名入表(扩列);d16b int() 包裹 | wd16 等价类/非行为改动 | +| `game/core/status_container.gd` | d16b int() 包裹四处(复用既有·修改) | 非行为改动 → 随 wd16 | +| `game/presentation/views/settlement_fx_view.gd` | d17 await 后守卫(复用既有·修改) | 行为改动(健态)→ wd17 | +| `game/presentation/views/board_view.gd` | d18 参数类型(复用既有·修改) | 非行为改动 → wd18 | +| `game/tests/unit/battle_settlement/damage_controller_test.gd` | d1 语义断言+d12 产发断言(复用既有·增断言) | 断言面 → wd1/wd12 | +| `game/tests/unit/presentation/standee_fx_sweep_test.gd` | **新增**:d2 意图档分档+d4 句柄 kill+d12 按 id 解析三断言族(形制=SceneTree+test_kit,同 modal_flow_test;新增·新文件) | 断言面 → wd2/wd4/wd12 呈现半 | +| `game/tests/unit/presentation/modal_flow_test.gd` | d9 三早退+合并制断言扩展(复用既有·增断言;:40-48 V6 序列原样保留同绿) | 断言面 → wd9 | +| `game/tests/unit/core/level_shop_economy_test.gd` | d11 断言扩展(复用既有·增断言) | 断言面 → wd11 | +| `game/tests/unit/presentation/pacing_queue_test.gd` | d12 排播链 target_id 优先断言(复用既有·增断言;夹具形态 T0 复核) | 断言面 → wd12 排播半 | +| `game/tests/unit/core/board_generator_test.gd` | d14 同值断言+调用签名同批改(复用既有·增/改断言——签名变更已申报契约 5) | 断言面 → wd14 | +| `game/tests/system_test.gd` | d6/d7 守卫两断言(复用既有·增断言) | 断言面 → wd6/wd7 | +| `game/tests/run_all.gd` | 新套件清单登记行(复用既有·增行) | 断言面接线 | +| `reports/iter-实现报告/代拍板追认单/反哺点`(三份,批末写) | 产出物落位(含 golden 判定读数) | 非行为改动 → wd19 | + +### 接口块(全卡共用) + +- 新成员/新函数签名(断言与撤销证明引用以此为准;**全类型含返回类型**): + - `standee_body.gd`:`var _intent_charge := false`;`func enemy_id() -> String`。 + - `battle_end_overlay.gd`:`var _celebrate_tw: Tween`。 + - `tune_sim_controller.gd`:`var _running := false`;`func is_running() -> bool`。 + - `battle_stage_view.gd`:`func _card_by_id(enemy_id: String) -> Node2D`(`_cards` 内 `c.enemy_id() == enemy_id` 相等匹配);`func enemy_standee_center(target_id: String, display_name: String) -> Variant`(id 先、名兜底;现行同位函数本无返回类型——新签名补 `-> Variant`,卡②落地后 S9 R2 面零新增)。 + - `config_db.gd`:`func shop_relic_base_price() -> float`/`func shop_spend_discount() -> float`/`func hero_display_name() -> String`。 + - `shop_table.gd`:`@export var relic_base_price: float = 15.0`+`@export var spend_discount: float = 0.8`(validate 域检两条);`hero_table.gd`:`@export var display_name: String = "探险者"`(非空域检)。 + - `board_generator.gd`:`static func target_mine_count(params: Dictionary) -> int`(命名 L1 可调);`legality(b: BoardData, params: Dictionary) -> bool`(去 db 参)。 + - `action_economy.gd`:`const CHARGE_LAYER_CAP := 3`。 +- row_pacer 两处取用改:`var target_id := String(row.get("target_id", ""))`+`stage.enemy_standee_center(target_id, target)`。 +- 新单测形制:`standee_fx_sweep_test.gd`=`extends SceneTree`+`const Kit := preload("res://tests/unit/test_kit.gd")`+`_init()` 逐断言+`quit(Kit.finish(...))`(modal_flow_test 同构);run_all 清单行 `{"path": "res://tests/unit/presentation/standee_fx_sweep_test.gd", "expect": 0}`。 +- 最窄闸(撤销证明跑法与判红口径): + - 单测族=`D:\code\godot\Godot_v4.7.2-stable_win64_console.exe --headless --path game --script res://tests/unit/<相对路径>.gd` 单件直跑;**运行期脚本错判红=直跑输出经 `tools/error_scan.py` 扫出 SCRIPT ERROR 标记**(F20——单件退出码不含脚本错,d2/d8 撤销半依赖此口径); + - 系统测试=`python tools/run_system_test.py`; + - 信号载体扫描=完成定义 3 的 `grep -rnP` 命令零命中; + - 类型面=`python tools/audit_type_hints.py`(判据=d18 行零命中+新增码零新增红)。 + +### 落位与可复用性预检(本批新增件逐条三问) + +- **`standee_fx_sweep_test.gd`(新单测文件)**——复用面:d9/d11/d14 各归域既有宿主(modal_flow/level_shop_economy/board_generator_test,F20);d1 归 damage_controller_test;d6/d7 归 system_test;d12 排播半归 pacing_queue_test;d2/d4/d12-站桩解析半三件同属「战斗舞台呈现域」且全库无 standee/battle_end/stage 宿主(实查 tests/ 树)——并入任一现役文件都域错位,新立=该域唯一断言宿主。唯一面:与 type_glow_test(墨色投影域)扫域不交。分层与交互面:纯呈现层直建(StandeeBody/BattleEndOverlay/BattleStageView 均 `preload().new()` 直构、零场景文件依赖——theme_projection_test:200 BoardView 直建先例同构)。 +- **ConfigDB 三个新访问器+两表三字段**——复用面:标量 knob+域检+访问器三件套是 ShopTable.price_a/price_b 既有形态(F11),照式扩展即复用;为什么不并入他表:relic 基价与散折都是商店域 knob、英雄名是英雄基线面——落点各归其主无第二候选。唯一面:三值各只此一处表内定义,level_runner/unit_container 字面量随批消亡。分层:数值只经 ConfigDB 单点读取(D3)。 +- **产品码 18 处修改**——零新增文件;新增函数仅访问器×3+常量×1+`_card_by_id`(为什么新增:按键解析与按名解析是两条判据,合一则旧路无兜底;与 `_card_by_name` 同文件同职责域无散落)。 +- **d14 `target_mine_count` static 函数**——为什么新增:两处手抄公式的唯一收口点;不能复用现成:全库该式仅此两处无第三处共享面。 + +### T0 事实坐实(≈0.25d) + +- [ ] 复核 §3 全部 file:line(重点:d12 产发端四址+orchestrator:718 treasure_used 行 target 值形态核类+:168/:400 fx 载荷键是否下发到 :308 解析口——判 target_id 增键的必要位点;pacing_queue_test 夹具形态;28 处信号载体清单逐处过目确认零误匹配)。可核查结果:行号漂移清单(零漂移则声明)进实现报告素材。 +- [ ] 采集改前读数基线(单测/系统测试通过数)。可核查结果:基线读数在案。 + +### T1 core 域缺陷修+断言(≈1–1.25d) + +- [ ] 实装 d1(is_same)+damage_controller_test 扩展语义断言:**手工构造**内容全同双敌字典 A/B(复刻单位字典键集但不含 handle_id——生产不可达的语义面用夹具钉住),A 开单(source_unit=A)→ B 死亡事件 → 断言 A 单据未作废;A 死亡 → A 单据作废(正向对照)。可核查结果:断言绿。 +- [ ] 实装 d11(两 knob+域检+访问器+level_runner 读表+注释订正)+level_shop_economy_test 扩展断言:`db.shop_relic_base_price() == 15.0`、`db.shop_spend_discount() == 0.8`(经 ConfigDB 真装载)+pool_price("relic") 同种子同输入现价与改前逐位一致(等价证明)。可核查结果:断言绿。 +- [ ] 实装 d16a(HeroTable.display_name+域检+访问器+unit_container:55 读表)+断言:`db.hero_display_name() == "探险者"`+spawn 后英雄单位 display_name 取值同。可核查结果:断言绿。 +- [ ] 实装 d15(常量化两处+注释随动)+d16b(int() 包裹六处)+d14(公式单点+legality 去参+两调用点改)+board_generator_test 同值断言(`target_mine_count` 与原式同参同值)+调用签名同批改。可核查结果:单测绿。 +- [ ] 实装 d10(minesweeper 两契约+全库 28 处载体统一)。可核查结果:完成定义 3 grep 读数零。 +- [ ] 实装 d13(孤儿注释删)。可核查结果::51-66 段注释与函数体对齐。 +- [ ] 撤销证明(**只回退本批 hunk**→最窄闸红→精确还原→记录;最窄闸=接口块单测直跑+error_scan 判红口径):①回退 d1 hunk(is_same 回 `==`)→ 双敌夹具断言红(B 死亡误废 A 单据);②回退 d10 契约 hunk → grep 命中 2 处红。等价类申报:d11/d16a/d14/d15/d16b/d13 撤销不红属预期(值等价/等价重构),以等价断言+改前改后读数对拍承载,记录表如实标注。记录表进实现报告。 +- [ ] 验收步骤:T 段出口跑一次全套闸(按域选档,本批只碰 `game/`),期望全绿(含 S13 基线零新增)+11 号闸 NEED-APPROVAL 留痕(触 face=data/tables+damage_controller,§0)。 + +### T2 呈现域缺陷修+断言(≈1–1.25d) + +- [ ] 实装 d2(`_intent_charge` 分档)+d3(ui_kit:28 括号引述整段删)+standee_fx_sweep_test 断言①:`set_pending("damage", "heavy")` → `_intent_charge == true`;`set_pending("damage", "非蓄力id")` → false;`set_pending("buff", …)` → false。可核查结果:断言绿。 +- [ ] 实装 d4(句柄 kill)+断言②:直建 BattleEndOverlay 入树 → `open_victory(...)` 两次 → 断言任一时刻活跃 celebrate tween 至多一个(旧句柄非 running/已 kill)→ `close()` 后 `_celebrate_tw` 无效或空。可核查结果:断言绿。 +- [ ] 实装 d5/d6/d7/d8(互斥四口)+断言③:直建 DebugOverlay → `set_sim_running(true)` → `_dbg_btn.disabled == true`;TuneSimController 假 panel+假 driver(:16 注入位)跑 `run(2)`,driver 回调内记录 `is_running()` → 断言运行中 true、收尾 false。system_test 两断言:**wd7**=真实冒险**关间态**(is_adventure=true 且 settlement==null——选路停点态,非战斗中;战斗中调用则守卫首条件已拦、断言空转)直调 `_on_tune_simulate(3)` → `_tune_sim.is_running()` 保持 false(d7 守卫早退;回退 d7 hunk 后 is_running 变 true 即红——调用后立即置回 `_tune_sim._running = false` 清理);**wd6**=闲时态白盒置态 `main._tune_sim._running = true` → 直调 `_start_debug_levels()` → 断言 `run_scope.settlement == null` 且开始层未被收起(守卫早退)→ 清理置态(回退 d6 hunk 后守卫漏模拟态即红)。可核查结果:断言绿。 +- [ ] 实装 d9(fail-fast+合并制)+modal_flow_test 扩展断言④:未登记层 open → 已开层 visible 全不变+该层不可见;A/B 双层开 A(B 被隐)→ 重复 open A → 断言 B 仍隐(V6 语义)→ close A → **B 恢复可见(合并制吞账修复)**;未登记层 close → 零副作用;:40-48 V6 在役序列原样绿。可核查结果:断言绿。 +- [ ] 实装 d12(产发端增键+standee 访问器+`_card_by_id`+`enemy_standee_center` 改签名+`:308` on_step_row_fx 解析同改+row_pacer 取用)+断言⑤:两 StandeeBody 同 `display_name`(如「哥布林」)异 `enemy_id` 挂 `_cards` → 按 id 各自命中(两口各断言一次);id 空时按名兜底行为同旧;damage_controller_test 扩:开单后行/载荷 `"target_id"` == 目标敌 enemy_id;pacing_queue_test 扩:row 携 target_id 时解析调用传 id(假 stage 记参断言)。可核查结果:断言绿。 +- [ ] 实装 d17(await 后守卫)+d18(lumas_out 类型)。可核查结果:`python tools/audit_type_hints.py` 对 board_view:998 零命中。 +- [ ] 撤销证明:③回退 d2 hunk → 断言①直跑输出经 error_scan 扫出 SCRIPT ERROR 红(`_intent_charge` 未定义);④回退 d4 kill 段 → 断言②活跃句柄数红;⑤回退 d5 hunk → 断言③ disabled 半红;⑥回退 d8 hunk → 断言③ is_running 半 error_scan 红;⑦回退 d9 合并段 hunk → 断言④ B 恢复半红(吞账复现);⑧回退 d12 站桩半 hunk → 断言⑤红。等价类申报:d3/d13/d17/d18 撤销不红属预期(注释/守卫惯例/类型面,如实标注——d17 以走查承载)。记录表进实现报告。 +- [ ] 验收步骤:T 段出口跑一次全套闸(按域选档),期望全绿。 + +### T3 渲染闸 golden 判定+产出物落位卡(≈0.5d,末卡) + +- [ ] golden 判定(契约 6):跑渲染闸读棋盘三图+菜单 changed 读数×4,按 README 双态判定;changed>0 落棋盘区=修对的证据,显式呈报后定重制+复闸。可核查结果:读数与判定结论进实现报告。 +- [ ] 轻闸全绿后、合并前写齐三份:`reports/iter-实现报告-<完成日>.md`(各单元状态+撤销证明记录表〔含等价类申报行〕+golden 判定读数+实机目检总清单〔蓄力敌意图三档观感/同名敌受击闪白与弹道落点两条〕+遗留读数义务)/`reports/iter-代拍板追认单-<完成日>.md`(§10 白名单内代拍项留痕)/`reports/iter-反哺点-<完成日>.md`(§6 各条有落点)。可核查结果:三文件在 `reports/` 且逐条有落点。 +- [ ] 合并前置:独立验收审计完成且报告 `reports/iter-验收审计-<完成日>.md` 逐行「通过」——未通过不得合并(审计入口=未参与实现的会话/子代理,只读本卡、实现报告对照表、diff 与测试件,自跑最窄抽查,不采信「已全绿」)。 + +**估时合计 ≈3–3.5d**(T0 0.25+T1 1–1.25+T2 1–1.25+T3 0.5)。切分预案:T1/T2 各含一次全套闸出口,超软盒时 d12(跨 core/presentation/排播三面)可独立成波。**编排注记**:本卡先行于 R1–R3 重构批落地(§0);与卡②先后皆可、d18 已替其消一件存量红;估时低于 iter-plan 四查③硬下限,与同会话他卡同批编入。 + +## 8. 验证闸 + +### 8.1 逐需求点验收清单 + +| # | 需求点 | 测什么 | 落点 | +|---|---|---|---| +| V1 | 同内容双敌单据不作废(d1,语义加固) | 夹具双敌:B 死亡不废 A 单据;A 死亡废 A 单据 | damage_controller_test 扩 | +| V2 | 蓄力档可达(d2/d3) | `_intent_charge` 两态;非蓄力零置位 | standee_fx_sweep 断言① | +| V3 | 庆祝 tween 不叠加(d4) | 活跃句柄至多一;close 即 kill | standee_fx_sweep 断言② | +| V4 | 批量模拟互斥闭合(d5–d8) | 调试钮随模拟置灰;控制器运行态可查;两守卫早退 | standee_fx_sweep 断言③+system_test | +| V5 | 模态栈 fail-fast+吞账修复(d9) | 未登记层零副作用;重复 open 不吞遮挡账;V6 在役断言同绿 | modal_flow_test 扩 | +| V6 | 信号契约成文(d10) | 两契约在位;全库载体 grep 零 `#` 命中 | grep+走查 | +| V7 | 数值入表(d11/d16a) | 表值==原字面量;消费端读表;现价逐位等价 | level_shop_economy_test 扩+单测 | +| V8 | 站桩按键解析(d12,两口) | 同名异 id 两口各自命中;行携 target_id;排播链传 id | standee_fx_sweep 断言⑤+pacing_queue_test 扩+damage_controller_test 扩 | +| V9 | 等价重构不破坏(d13–d18) | 公式单点同值;调用点改签名后套件绿;int() 包裹后套件绿;守卫/类型补齐 | board_generator_test+全量闸+S9 | +| V10 | golden 判定 | changed 读数×4+判定结论 | 渲染闸+README 记录 | +| V11 | 反哺点清单 | §6 各条有落点 | 件核对(T3 出口) | + +### 8.2 改动—验收对照表 + +> 断言 desc 统一尾缀〔wd#〕;撤销证明逐条对应 T1/T2 撤销步;**证明纪律=2026-10-24 P0 令(只回退本批 hunk,禁注入造红)**。 + +| 需求点 | 改动 | 验收测试(尾缀) | 层级 | 真实入口 | 撤销证明 | +|---|---|---|---|---|---| +| wd1 双敌单据(语义加固) | `damage_controller.gd:215` 判定符 | 夹具双敌两断言〔wd1〕damage_controller_test 扩 | 单元 | 控制器真实构造(ConfigDB/RngService/UnitContainer 全链,F20 先例)+`unit_died.emit` 真实事件;夹具=生产不可达语义面的显式钉住(F1 定性如实注记) | 回退 d1 hunk → 夹具断言红(T1①)——生产盘面撤销不红属预期,如实申报 | +| wd2 蓄力档 | `standee_body.gd:171-194` 布尔分档 | 断言①〔wd2〕standee_fx_sweep_test | 单元 | `set_pending` 真实调用链(快照投影 pid) | 回退 d2 → error_scan 红(T2③) | +| wd3 注释订正 | `ui_kit.gd:28` | 走查(注释面) | — | — | 不适用(随 wd2 复核) | +| wd4 庆祝句柄 | `battle_end_overlay.gd:281-287/156-161` | 断言②〔wd4〕standee_fx_sweep_test | 单元 | `open_victory`/`close` 真实开合链 | 回退 kill 段 → 红(T2④) | +| wd5 调试钮 | `debug_overlay.gd:84` | 断言③ disabled 半〔wd5〕 | 单元 | `set_sim_running` 真实信号路径 | 回退 d5 → 红(T2⑤) | +| wd6 调试开局守卫 | `main.gd:712-717` | system_test 白盒置态断言〔wd6〕 | 系统 | 守卫入口直调+真实闲时态(`_tune_sim._running` 白盒置态即真实运行态位面) | 回退 d6 → 守卫漏模拟态 → settlement 非 null 红(T2②wd6) | +| wd7 模拟守卫 | `main.gd:1489` | system_test〔wd7〕 | 系统 | 真实冒险关间态(is_adventure 且 settlement==null)直调 `_on_tune_simulate` | 回退 d7 → is_running 变 true 红(T2②wd7) | +| wd8 运行态面 | `tune_sim_controller.gd` | 断言③ is_running 半〔wd8〕 | 单元 | `run()` 真实执行体+driver 注入位 | 回退 d8 → error_scan 红(T2⑥) | +| wd9 模态 fail-fast+合并制 | `modal_stack.gd:21-38` | 断言④四半〔wd9〕modal_flow_test 扩+:40-48 V6 同绿 | 单元 | ModalStack 直构+哑 Control(宿主先例) | 回退合并段 → B 恢复半红(T2⑦) | +| wd10 信号契约 | `minesweeper_rules.gd:8-9`+28 行 | grep 零命中+契约走查〔wd10〕 | 静态扫描+走查 | 发射位实查(:129/:303/:310/:345/:476) | 回退 d10 契约 → grep 红(T1②) | +| wd11 数值入表 | shop/hero 表+config_db+level_runner/unit_container | 表读断言+等价断言〔wd11〕level_shop_economy_test 扩+单测 | 单元 | ConfigDB 真装载+pool_price/英雄构造真实消费链 | 等价类申报:撤销不红属预期(原值迁移)——等价断言+读数对拍承载 | +| wd12 按 id 解析(两口) | `damage_controller:168/370/400/504`+`standee_body`+`battle_stage_view:308/:342`+`row_pacer` | 断言⑤(两口)+产发断言+排播断言〔wd12〕三宿主 | 单元 | 站桩装载链(set_data enemy_id)+fx/排播真实行驱动 | 回退站桩半 → 断言⑤红(T2⑧) | +| wd13 孤儿注释 | `treasure_items.gd:55-62` | 走查(注释面) | — | — | 不适用(非行为改动) | +| wd14 公式单点 | `board_generator.gd:97/334/281` | 同值断言+套件绿〔wd14〕board_generator_test | 单元 | 生成期/断言期两真实调用点 | 等价类申报:同值断言为判据,撤销不红属预期 | +| wd15 常量化 | `action_economy.gd:185/191-192` | 全量闸回归线〔wd15〕 | 集成 | finish_round_stop 真实结算链(既有套件覆盖) | 同 wd14 性质(等价申报) | +| wd16 英雄名/Variant | `unit_container:55/257/270`+`status_container` 四处+hero 表 | 表读断言+套件绿〔wd16〕 | 单元 | spawn/apply_damage/heal/apply_effect/decay 真实链(既有套件覆盖) | 等价类申报(同 wd11) | +| wd17 await 守卫 | `settlement_fx_view.gd:94-95` | 走查+惯例一致性〔wd17〕(申报:无独立断言) | 走查 | play_damage_number 真实演出链 | 申报豁免(T2 如实标注) | +| wd18 参数类型 | `board_view.gd:998` | `python tools/audit_type_hints.py` 该行零命中〔wd18〕 | 静态扫描 | 三调用点真实形态(F19) | 回退类型 → S9 R3 命中红 | +| wd19 产出物 | T3 三份 | 件核对〔wd19〕 | — | — | 删一落点 → 件核红 | + +**测试方式两分法**:V1–V9 全机检;能力验证经真实构造/事件链驱动(调试接口零用)。**用户目检项**(登记不阻塞):蓄力敌(heavy 技能敌)意图三档观感+同名敌受击闪白与弹道落点两条,写进实现报告目检总清单。 + +**闸口径**:轻闸每任务卡出口自跑;批末重闸=`tools/run_all_gates.sh` 按改动域选档(本批只碰 `game/`;域划分唯一出处=脚本头注)全绿才合并+改前改后读数对拍;11 号闸触面 NEED-APPROVAL 留痕不判红(§0);S13 gdstyle 基线零新增(§0)。回归措辞:里程碑步出口一次全套;波内修复只做最窄确认;新增测试先行孤立验证再并入。 + +### 8.3 测试削弱申报(test-conventions §9.5) + +零削弱;唯一既有测试改动=board_gen_reward_test:145/board_generator_test 的 `legality` 调用签名随 d14 同批改(签名变更非削弱,申报在案);既有断言零删除零放宽——**modal_flow_test :40-48 V6 序列原样保留且必须同绿(契约 5)**。 + +**独立验收审计(test-conventions §9.7)**:合并前由未参与实现的会话/子代理审计——只读本卡、实现报告对照表、diff 与测试件,自跑最窄抽查;报告落 `reports/iter-验收审计-<完成日>.md`,未通过不得合并。 + +## 9. 异常分级 + +- **L1(自救)**:`target_mine_count`/`_card_by_id`/`standee_fx_sweep_test` 命名与签名细节(接口块签名语义不变前提);d10 契约措辞(三要素不变);断言夹具构造细节。 +- **L2(记录继续)**:d9 合并制残边注记——上层「其间被重新 open 又被显式 close」的 stale 遮挡记录会在 close 下层时复活,现调用面与 V6 流不触及、接受为域外(第二轮复核注记在案);orchestrator:718 treasure_used 行 target 形态扩入 d12 产发端清单;:168/:400 fx 载荷键下发面核类结果;pacing_queue_test 夹具与假 stage 形态适配;跨表校验器新字段登记与否;wd6 白盒置态的清理写法。 +- **L3(停机呈报)**:夹具双敌字典构造不出(单位键集复刻受阻);golden 判失效且重制面涉及非本批像素;发现钉值表现状与 §3 不符(含 28 处清单漂移);任一既有套件出非本批存量红;d11/d16a 等价断言不成立(表迁移后现价/构造逐位不一致——数值面意外变化);modal_flow_test V6 序列与 d9 修法无法同绿(合并制失效——回呈修法裁决);任何要求顺带改数值/编排逻辑/⑦头注的诉求。 + +## 10. 代拍板白名单(超清单必呈报) + +1. `target_mine_count`/`_card_by_id`/`standee_fx_sweep_test` 命名与签名细节(接口块签名语义不变前提)。 +2. d10 契约成文措辞与载荷键集并集的取舍(以实查为准)。 +3. d12 兜底路径形态(id 优先+名兜底 vs 纯 id——倾向前者,兜底承 fx 行类)。 +4. d16b int() 包裹的写法(读侧包 vs 写侧包——语义等价取一贯)。 +5. 提交切分与 worktree 分支命名。 + +## 11. 待裁点 + +**零待裁点,开工门=U0。** + +五处灰色地带已按设计事实推导定值(用户可否决,随 T3 反哺点清单逐条列示呈报): +- **d1 定性为语义加固而非行为修复**——依据=handle_id 生产不可达实证(F1)+iter36 P0 纪律下「生产盘面撤销不红」须如实申报;备选「按审计原述定性为行为缺陷」淘汰依据=审计前提读数被 unit_container:98 排除(本卡 §3 已订正回呈)。 +- **d12 用 enemy_id 增键而非槽位 index**——依据=单位字典自带 enemy_id(F12)零新造键语义、槽位随编排重排会漂;备选「槽位 index」淘汰依据=时序耦合。 +- **d11 走标量 knob 不扩 SHELF_KINDS 词表**——依据=relic 是池令牌非货架种(F11 注释自证)、扩词表属数值批设计裁;备选「pool_shelf 增 relic 行」淘汰依据=词表封闭性+现价公式 relic 专枝已存在(:894)。 +- **d15 蓄力上限走模块常量不入表**——依据=机制结构常量先例(STANDEE_FOOT_MARGIN 同型);备选「入 action_economy.tres」淘汰依据=审计未判其为玩法数值、无调参诉求。 +- **d16a 英雄名入 HeroTable**——依据=敌人名对称先例(row.display_name)+文案值表内单源先例(settlement.tres source_label_map);备选「保留字面量」淘汰依据=审计在册缺陷定性。 + +## 12. 自审六查与 grilling 复核记录 + +**自审六查(2026-10-24 写卡会话)**:①规格覆盖——19 单元逐条挂 T1/T2/T3 并指到 wd1–wd19;②步骤扫描——一步一动作一可核查结果,零函数体;③类型一致性——接口块签名与 F1–F19 实查逐字一致(v1.1 补全返回类型与参数类型);④审查焦点+改动覆盖——补挂:同内容双敌(wd1 正反两断言+定性申报)、两口解析(wd12 两半)、等价断言(wd11/wd14/wd16)、golden 修对证据面(契约 6)、d17 无断言如实申报;⑤篇幅比例——钉值表与 T 卡≈1:1,零代码块;⑥落位与复用——唯一新增文件 standee_fx_sweep_test 的域归属与既有宿主分工已答(预检节),两表三字段各归其主。 + +**grilling 第一轮复核记录(2026-10-25,独立子代理对抗复核)**:判回炉三阻断——A-1 d1 缺陷前提生产不可达(unit_container:98 handle_id——验收链「真实构造+撤销」自相矛盾);A-2 d9 幂等直返与 modal_flow_test :40-48 V6 在役断言冲突;A-3 d12 漏 `battle_stage_view.gd:308` on_step_row_fx 第二解析口(审计「闪白/击杀」半不修)。随同九应修(A-4 iter36 已入库陈述过时+第三卡漏记;A-5 接口块两处缺类型;A-6 改 .tres 值造红违 P0 令;A-7 脚本错红判出口径缺 error_scan;A-8 wd6 无断言支撑;A-9 d3 两引用址实为失真;A-10 grep 命令 -P 订正;A-11 fx 载荷与行的措辞;A-12 d15 注释随动)+两建议。v1.1 全部折入:d1 重定性(语义加固+夹具断言+生产盘面撤销申报)、d9 改合并制(V6 同绿硬约束入契约 5)、d12 扩两口、§0 换代(iter36 入库/S13 gdstyle/P0 令出处)、接口块全类型、撤销步全改「只回退本批 hunk」形态、wd6/wd7 补真断言、F1/F3/F9/F10/F12/F20 六条事实按 grilling 补查订正。复核结论:三阻断闭合、九应修落位(v1.1)。 + +**grilling 第二轮复核记录(2026-10-25,同代理续核)**:总判**通过**——d1 重定性自洽(夹具钉语义+生产盘面撤销申报三件自洽)、d9 合并制经 modal_flow_test :40-60 全序列推演与 V6 同绿、d12 两口齐、九应修全折入。拟议的「d9 push_error 是否误触 error_scan」经在库反证消解(config_db_fatal_oob_test 成功路径必经 push_error 而长期绿——本引擎 push_error 输出不含 SCRIPT ERROR 标记,非问题)。余四条建议级:分类口径统一(完成定义 2 d17 单列)、wd7 时点钉死关间态、d9 残边注记(L2)、wa8→u7–u9 与应修计数订正——v1.2 已全部折入。零阻断零应修,卡进可编排态。 diff --git a/reports/需求卡片-信息迷雾可见性批-2026-10-21.md b/reports/需求卡片-信息迷雾可见性批-2026-10-21.md deleted file mode 100644 index 1d5788b5..00000000 --- a/reports/需求卡片-信息迷雾可见性批-2026-10-21.md +++ /dev/null @@ -1,246 +0,0 @@ -# 需求卡片:信息迷雾可见性批(关卡节点图重构卡④——选路信息可见性落地) - -- 版本:**v1.0**(2026-10-21 成文;grilling 复核结论见 §11) -- 来源:[待办清单](../待办清单.md)「关卡节点图重构——可选节点太少」(2026-10-13 登记,设计面=文档轨)(唯一事实源:① [关卡节点地图](../design/gdd/systems/冒险循环/关卡节点地图.md) v2.1「选路信息可见性:近处见类型,远处起雾」节+同文调参表「信息可见层数 N」;② [关卡节点图生成](../design/gdd/systems/冒险循环/关卡节点图生成.md) v1.1「下一跳必在可见范围,是决策的可见锚」承诺;呈现口径权威=[level-map](../design/gdd/ux/level-map.md) 头部机制权威行+节点态呈现口径〔视觉终稿冻结件〕) -- 归属:关卡节点图重构批**卡④**(可见性规则+呈现接线);卡①生成器/卡②行进换代/卡③敌人表不在本卡 -- 排期:未编入(2026-10-18 用户令:需求卡片不预占迭代号,批次归属按工作量另裁) -- 开工门:U0(用户 `/iter` 发令)+ grilling 复核门过+零待裁点(§11) - -## 0. 前置依赖与并行性 - -- 设计闭合凭据=关卡节点图生成.md v1.1+关卡节点地图.md v2.1(2026-10-21),06 条目状态「文档设计已闭合」——第〇步放行成立。 -- 本卡消费的节点图数据形态=现行 `level.tres` 模板池(type/node_depth/next 三键);卡①生成器落地图后数据形态不变(生成侧只换图的来源),本卡消费面零随动。 -- **06 状态回写呈报**:条目状态应推进为「已写卡(`reports/需求卡片-信息迷雾可见性批-2026-10-21.md`)」——本会话受「只许创建/修改本卡一个文件」约束未落,开工前须由用户补记或授权写卡方补记(登记不阻塞,防遗忘义务在此显式承载)。 -- 与在役结构的关系:`LevelRunner`(core)与 `LevelMapScreen`(presentation)均在役,本卡只扩只读出口与呈现决策面,不改 autoload 链、不改行进/兑付流程。 - -## 1. 目标与完成定义 - -**目标**:把关卡节点地图 v2.1「选路信息可见性」三层语义落成可见性规则+最小显隐呈现——图结构全图明牌(节点、连线、入口、终点位置永远可见)/节点类型仅在邻近 N 层内可见、范围外信息迷雾只见结构不见类型/节点内详情(打谁、给什么)图上从不显示;Boss 标记与终点身份不入雾。N 进数据表(provisional 挂值,数值定值归定值批)。呈现接线三件:迷雾下节点显隐语义、多终点 Boss 标记、画布自适应(图变大布局跟上)。 - -**相对现状,新增了什么**: -1. 可见性规则(现役图上类型全明牌、零过滤——见 F2):`LevelRunner` 只读出口 `type_visible_ids()`; -2. N 参数落数据表(现役无此字段——见 F6):`LevelTable.info_visible_layers`(int ≥ 1,provisional 2); -3. 迷雾呈现显隐接线(现役图标/类型文字无条件绘制——见 F2):`node_display` 决策面+`_draw_graph` 消费; -4. 多终点 Boss 标记接线(渲染循环已通用,雾豁免与夹具断言补齐——见 F7); -5. 画布自适应断言面(公式已通用,大图回归断言与探针常数锚同源补齐——见 F3)。 - -**完成定义**(全部满足才算本批完成): -1. 邻近 N 层内节点类型可见;层距 > N 的非豁免节点只见结构(方块态色/编号/连线)不见类型(图标+类型文字);N 读数据表、int ≥ 1; -2. Boss 标记与终点身份恒可见(多终点图每个终点均带标记且均不入雾);当前候选(下一跳)恒可见类型; -3. 节点内详情图上零显示(回归断言在案:决策面不消费内容字段); -4. 迷雾呈现=显隐语义,零新画法零灰化遮罩;冻结视觉件零改动; -5. 单圈语义:可见性纯函数于当前锚点,已过节点出圈回雾(零累积记忆); -6. 画布自适应对大图成立(包络覆盖最深节点、命中可达)+探针常数锚与屏内公式同源; -7. 断言批全绿+变异验证记录齐;三份 `reports/` 产出物写齐(T5);批末代码域闸全绿+改前改后读数对拍。 - -## 2. 现状事实(2026-10-21 worktree 实查;开工日 T0 复核行号) - -| # | 事实 | 出处 | -|---|---|---| -| F1 | 节点态四色+Boss 描边在 `_draw_graph`:已过=COL_ROOM_DONE(暗金 #68562C)/当前=COL_ROOM_CURRENT(金)/未达=COL_ROOM_FUTURE(暗);Boss=紫赤 2px 描边+外缘 1px 深墨环(灰度冗余通道);候选=亮金描边(悬停 4px/常态 2px) | `game/presentation/views/level_map_screen.gd:10-15`/`:450-464`(态色 `:450-454`、Boss `:457-461`、候选 `:462-464`) | -| F2 | 节点类型三通道全明牌、零可见性过滤:字形回退 NODE_LABELS(战/赏/疗/王/商)、类型文字 NODE_TYPE_NAMES、五键图标 TEX_NODE_ICONS;每个节点无条件绘制图标(或字形)+编号+类型文字标签(当前位转「当前位置」、候选加「· 可选」) | `level_map_screen.gd:17-20`/渲染点 `:467-475`(图标/字形)、`:477-479`(编号)、`:482-489`(类型文字) | -| F3 | 画布自适应已公式化通用:布局常数 NODE_HALF=36.5×25(52×36 的 1.4×)、LAYER_BASE_X=70/LAYER_STEP_X=122/SLOT_BASE_Y=52/SLOT_STEP_Y=58;`_fit_canvas` 包络=宽 70+最大层×122+NODE_HALF.x+8 × 高 52+最大槽×58+NODE_HALF.y+24,下限 620×256;`_layout` 与命中 `_candidate_at` 同源 | `level_map_screen.gd:25-29`/`:289-304`/`:492-501`/`:373-386` | -| F4 | 呈现只读出口在役:`nodes()`/`next_candidate_ids()`/`current_node_id()`/`node_id_of()`/`node_out_ids()`/`visited_ids()`;无可见性出口(本卡新增) | `game/core/level_runner.gd:327`/`:388`/`:396`/`:401`/`:405`/`:410` | -| F5 | 编号空间=图形态 node_depth/单链数组下标(`_node_id`);层=入口 BFS 深度(`_assign_layers` 分层=画布列同源),同层 slot 垂直排布 | `level_runner.gd:340-343`/`level_map_screen.gd:517-542` | -| F6 | 数据面无 N 字段:LevelTable 仅 `rows`;rest_heal_pct 越域校验先例([0,100] domain 错)可循;level.tres 三行各带 rest_heal_pct=30 | `game/data/schemas/level_table.gd:7`/`:35-37`/`game/data/tables/level.tres` | -| F7 | 图合法性现役「Boss 恰一」(boss_count != 1 → structural 错)——多终点图现役被加载拒;但渲染循环逐节点判 `type == "boss"` 已天然多 Boss 通用 | `game/data/schemas/level_table.gd:80-81`/`level_map_screen.gd:457-461` | -| F8 | 冻结件现状:level-map 视觉终稿已冻结(2026-09-28 样张 v1 机检 11/11);画布冻结公式「宽 = 70+层深×122+34、高 = 52+槽位×58+26+8」与代码现值漂移(代码宽尾 44.5=NODE_HALF.x+8、高尾 49=NODE_HALF.y+24;节点 1.4× 化+标签行 +24 后所致,注自述「层距/布局公式零改」) | `design/gdd/ux/level-map.md:3`/`:68`/`level_map_screen.gd:25`/`:301-303` | -| F9 | Boss 标记口径两文档不一致:关卡节点地图.md:67 写「Boss 封印黑描边(旗朱→封印紫→封印黑 #000000)」,level-map.md 写「紫赤描边+深墨环」;**代码实况=后者**(紫赤 BOSS_PURPLE 描边 2px+INK_ROUTE 深墨外环 1px) | `design/gdd/systems/冒险循环/关卡节点地图.md:67`/`design/gdd/ux/level-map.md:77`/`:113`/`:175`/`level_map_screen.gd:457-461`/`game/presentation/views/ui_kit.gd:19`(INK_ROUTE)、`:43`(BOSS_PURPLE) | -| F10 | 节点内详情现状已零显示:`_draw_graph` 只消费 type/节点 id/访问态三类信息,图上无敌人行集、奖励内容渲染——「图上从不显示」现状成立,缺回归断言 | `level_map_screen.gd:420-489` | -| F11 | 画布拟合探针常数锚手抄数字与屏内公式漂移:探针 `need_x2 = 70+maxl×122+26+8`,屏内公式尾为 NODE_HALF.x+8=44.5——探针断言被低报 10.5px 仍绿(自述「改常数须两侧同步」) | `game/smoke/ui_input_probe.gd:98-103`/`level_map_screen.gd:301` | - -## 3. 设计契约(红线,违反 = L3 停机呈报) - -🔴 **契约 1 · 可见性三层语义不破**:图结构(节点、连线、入口、终点位置、编号)全图明牌;节点类型=邻近 N 层内可见、范围外只见结构不见类型;节点内详情图上从不显示(关卡节点地图.md:97-101 三层表逐行兑现)。 - -🔴 **契约 2 · Boss/终点/候选三豁免**:Boss 标记与终点身份不入雾(豁免集=type=="boss",终点恒为 Boss);当前候选(下一跳)恒可见类型——兑现生成.md:83「下一跳必在可见范围,是决策的可见锚」承诺(直接兑现、不押注图形态)。 - -🔴 **契约 3 · N 只进数据表**:`info_visible_layers`(int ≥ 1)落 LevelTable,provisional 挂值 2;代码禁硬编码 N、禁绕表读值;数值定值权归定值批(调参表「provisional,定值批落数据表」)。 - -🔴 **契约 4 · 显隐语义、零新画法**:迷雾节点=类型图标+类型文字不绘制(显隐),结构通道(方块态色/编号/连线)照常;**禁灰化遮罩、禁雾纹理、禁任何新色值/线宽/形制**——雾的正式视觉规格属冻结件域,须用户裁(呈报项 §5),本卡只做可见性语义的最小呈现。 - -🔴 **契约 5 · 单圈语义**:可见性是当前锚点的纯函数(零累积记忆)——已过节点出圈回雾;锚层=当前节点层,入口停点(node_index=-1)锚层=0。 - -🔴 **契约 6 · 画布自适应不动冻结件**:公式族自适应保留(图变大布局跟上=包络随层深/槽宽增长);完整可见性由生成参数档背书(图包络 ≤ 屏预算,进反哺点移交卡①参数档);**禁缩放/滚动等动冻结视觉件手段**(若定值批需要超预算图,另行走呈报由用户裁)。 - -🔴 **契约 7 · 呈现只读不破**:可见性一律经 `LevelRunner.type_visible_ids()` 只读出口(纯函数、零状态、断点快照零新增字段);呈现层禁自算层距、禁触碰 runner 写面(level-map.md 架构注记)。 - -🔴 **契约 8 · 范围锁**:不动生成器/生成参数档/图合法性放宽(卡①域)、不改行进(卡②域)、不改敌人表(卡③域)、不改 N 数值(定值批)、不改冻结视觉件(用户域)。 - -## 4. 范围边界 - -**做**:可见性规则(层距 N+三豁免+单圈);N 落 LevelTable+level.tres;雾显隐呈现接线(`node_display` 决策面+`_draw_graph` 消费+探针);多终点 Boss 标记雾豁免接线+夹具断言;画布自适应大图断言+探针常数锚同源化;断言批(单测+冒烟+变异验证记录);反哺点清单。 - -**不做**(明示去向): -- 生成器、生成参数档、图合法性「Boss 恰一→多终点」放宽 → 卡①(生成域规则语义以生成产出规格为准);本卡呈现面按多 Boss 通用实现+合成夹具断言,卡①数据到位即接通; -- 行进换代(Peglin 式移动/选路方式) → 卡②; -- 敌人表 (level_index, node_depth) 行集随动 → 卡③; -- N 的数值定值 → 定值批(本卡只挂 provisional 2); -- **雾的正式视觉规格**(雾纹理/灰化遮罩/明度层) → 呈报项,用户裁冻结件后另立(level-map 视觉终稿已冻结,禁自行重开); -- level-map.md/GDD 正文改写(含 F8 公式漂移、F9 Boss 口径不一致的文档修正) → 文档批(走 §5 反哺点)。 - -## 5. 关联文档同步面 - -> **口径(2026-10-17 文档同步闸 v3)**:本批是代码实现批,禁写 `design/` 整目录与根规则文件,唯一可写目录 = `reports/`。下表「同步义务」一律执行为「列进 `reports/iter-反哺点-<完成日>.md`:哪个文件哪一节、改成什么、依据,用户会话裁决后归文档批落文」。禁写「随批」。 - -| 文档 | 关联点(本批实现会改变它的哪条陈述) | 同步义务 | -|---|---|---| -| `design/gdd/systems/冒险循环/关卡节点地图.md` | :172 调参表 N「定值批落数据表」→ 本批已落表(provisional 2);:100「可见范围(邻近 N 层)」的实现语义锚定(层距口径=入口 BFS 层差、三豁免、单圈回雾)需入文固化 | 反哺点 · 列清单(调参表状态行改「已落表 provisional 2」+可见性节补实现语义注记) | -| `design/gdd/systems/冒险循环/关卡节点图生成.md` | 调参表(:105-114)补「图包络 ≤ 屏预算」约束行(契约 6 依据:完整可见 AC 冻结 ⇒ 参数档背书);:83「下一跳必在可见范围」标注实现侧已兑现 | 反哺点 · 列清单 | -| `design/gdd/ux/level-map.md` | :68 画布公式与代码现值漂移核对(34→44.5/26+8→25+24);Information Hierarchy 补「类型可见性(邻近 N 层显隐)」层级行;**雾的正式视觉规格待用户裁后另立(呈报项,禁本批落文)** | 反哺点 · 列清单+呈报项登记 | -| `design/gdd/术语表.md` | 新词入册:「信息迷雾」(关卡节点图类型可见性)区别于「视野迷雾」(战斗棋盘,HUD框架/art-bible §6.5)——两系统同词根易混 | 反哺点 · 列清单 | -| `design/gdd/systems/冒险循环/关卡节点地图.md` :67 | Boss 描边口径「封印黑 #000000」与 level-map.md/现役代码「紫赤+深墨环」不一致(F9)——文档批核对择一 | 反哺点 · 列清单 | -| `待办清单.md` | 06 条目状态 →「已写卡(卡片路径)」 | **写卡段义务**(非实现批)——本会话单文件约束未落,开工前补记(§0 呈报) | -| `roadmap.md` | 迭代行编入笔与批内状态刷新 | 反哺点 · 列清单 | -| `game/core/level_runner.gd`、`game/data/schemas/level_table.gd`、`game/data/tables/level.tres`、`game/presentation/views/level_map_screen.gd`、`game/smoke/ui_input_probe.gd`、`game/smoke/smoke_all.gd`、`game/tests/**` | 实施本体 | 实施本体(可写) | - -## 6. 任务卡 - -> 估时按 Claude 工作日口径;每卡出口自跑轻闸;开工日 T0 复核本卡全部行号。 - -### 卡前·文件结构图 - -| 文件 | 职责 | 动作 | -|---|---|---| -| `game/data/schemas/level_table.gd` | N 字段承载+越域校验 | 改 | -| `game/data/tables/level.tres` | N provisional 挂值 | 改 | -| `game/core/level_runner.gd` | 可见性只读出口 `type_visible_ids()` | 改 | -| `game/presentation/views/level_map_screen.gd` | 雾显隐决策面 `node_display`+`_draw_graph` 消费+`display_probe` | 改 | -| `game/smoke/ui_input_probe.gd` | 拟合探针常数锚同源化(去手抄数字) | 改 | -| `game/smoke/smoke_all.gd` | 真图可见性冒烟断言 | 改 | -| `game/tests/unit/core/level_map_type_vis_test.gd` | 可见性规则单测 | 新建 | -| `game/tests/unit/presentation/level_map_info_fog_test.gd` | 显隐呈现+多 Boss+详情不显示单测 | 新建 | -| `game/tests/run_all.gd` | 两套件注册进 manifest | 改 | - -### T0 事实坐实与基线读数(≈0.5–1d) - -- [ ] 复核 §2 全部行号与读数,漂移即订正本卡(产出:本卡 §2 订正或维持记录)。 -- [ ] 跑 `godot --headless --path game --script res://tests/run_all.gd` 取改前基线读数(套件数/失败数)写进实现报告 T0 段。 -- [ ] 跑 `tools/run_all_gates.sh` 按改动域选档取改前闸读数写进实现报告 T0 段。 - -### T1 N 落表+核心可见性出口(≈1–1.5d) - -**接口声明**——产出:`LevelTable.info_visible_layers: int`(默认 2,validate 越域 domain 错);`LevelRunner.type_visible_ids() -> Array[int]`(语义见下)。取用:无(首卡)。 - -- [ ] `level_table.gd` 在 `rows` 声明(:7)后新增 `@export var info_visible_layers: int = 2`——核查:字段在案、类型 int。 -- [ ] `level_table.gd` `validate()`(:20 起)增越域检查:`info_visible_layers < 1` 时 append `{"category": "domain", "message": "info_visible_layers 越域 int≥1: %d"}`——核查:单测断言面可判红。 -- [ ] `level.tres` 资源顶层挂 `info_visible_layers = 2`——核查:`db.load_tables` 零错+读数 2。 -- [ ] `level_runner.gd` 新增只读出口 `func type_visible_ids() -> Array[int]`——语义钉死:返回集 = Boss/终点 id(type=="boss")∪ `next_candidate_ids()` ∪ {id : |层(id) − 锚层| ≤ `db.level.info_visible_layers`};锚层=当前节点层,无当前节点(入口停点)=0;层=入口 BFS 深度(与 `level_map_screen._assign_layers` 同源口径);纯函数零状态、断点快照零新增字段——核查:套件可调用且边界断言绿。 -- [ ] 新建 `game/tests/unit/core/level_map_type_vis_test.gd`(extends SceneTree+Kit.check 断言,仿 curio_refresh_paid_test 样式;断言 desc 尾缀挂 §7.1 对照表:①⑤→〔fv1〕、②→〔fv4〕、③→〔fv3〕、④⑦→〔fv7〕、⑥→〔fv6〕),七组断言:① 层距 N 内可见/N+1 不可见(白盒注入 level_row+停点态,仿 smoke_all ⑫ 白盒先例);② 候选恒显(夹具含跨层跳边候选——候选层距恰 N+1 仍显);③ Boss 恒显(远距 Boss);④ 单圈回雾(已过远节点回雾、visited 不豁免);⑤ 入口停点锚层 0+**单链退化形态**(L1 现役无 `next` 键图上层距判定同成立——出边按 `_out_ids` 数组顺序退化口径);⑥ N 读表(改 `db.level.info_visible_layers` 为 1/3 → 可见窗随动;N ≥ 全图层高 → 全部非豁免节点可见)+越域 validate 判 domain 红;⑦ 续玩重建同值(`start_level_external` resume_state 重建停点后 `type_visible_ids()` 与同停点直入路径逐位一致)——核查:套件退出码 0。 -- [ ] `run_all.gd` `_suites()` 增一行 `{"path": "res://tests/unit/core/level_map_type_vis_test.gd", "expect": 0}`——核查:run_all 套件计数与 manifest 条目数一致。 -- [ ] 变异验证:把层距判定 `<= N` 改成 `< N` 跑该套件见边界断言红 → `git checkout` 精确还原 → 记录进实现报告变异表(注入形态/闸/红输出/还原结论)。 - -### T2 雾显隐呈现接线(≈1–1.5d) - -**接口声明**——取用:T1 的 `type_visible_ids()`。产出:`LevelMapScreen.node_display(node, nid, type_vis, is_current, is_candidate) -> Dictionary`(static 纯函数)+`LevelMapScreen.display_probe() -> Dictionary`。 - -- [ ] `level_map_screen.gd` 新增 `static func node_display(node: Dictionary, nid: int, type_vis: Array, is_current: bool, is_candidate: bool) -> Dictionary`——返回 `{"draw_icon": bool, "draw_label": bool, "label_text": String, "boss_border": bool}`;语义钉死:`boss_border = (type == "boss")`;Boss 节点 icon/label 恒 true;非 Boss 节点 `draw_icon = draw_label = type_vis.has(nid)`;`label_text`=现役文案规则(当前位「当前位置」/候选「<类型名> · 可选」/余「<类型名>」)——核查:纯函数单测可直调。 -- [ ] `_draw_graph` 消费 `node_display`(每停点一次取用 `type_vis = _runner.type_visible_ids()`;图标/字形 `:467-475`、类型文字 `:482-489`、Boss 描边 `:457-461` 三处改读决策面输出);结构通道(方块态色/编号/连线)零改动——核查:现役图渲染冒烟不红。 -- [ ] `level_map_screen.gd` 新增 `func display_probe() -> Dictionary`(id → `node_display` 快照,只读断言面,仿 `canvas_probe` 先例)——核查:探针可调用。 -- [ ] 新建 `game/tests/unit/presentation/level_map_info_fog_test.gd`(断言 desc 尾缀挂 §7.1 对照表:①④→〔fv2〕、②→〔fv3〕、③→〔fv5〕、大图组→〔fv8〕),四组断言:① 显隐(雾下节点 draw_icon=false 且 draw_label=false,boss_border=false;可见节点全 true);② Boss 豁免+多 Boss 标记(双 Boss 夹具:两节点 boss_border=true 且均在 type_vis);③ 详情不显示(夹具节点额外携带内容键如 `enemy_id/reward_id`,断言 label_text 不含其值且决策面输出与不带内容键的同型节点逐键相等);④ label_text 文案三态(当前位置/· 可选/类型名)——核查:套件退出码 0。 -- [ ] `run_all.gd` 注册该套件(expect 0)——核查:manifest 计数一致。 -- [ ] 变异验证两发:① 雾下 `draw_icon` 强制 true → ①断言红;② label_text 拼接 `node.get("enemy_id")` → ③断言红;逐发「注入→最窄闸见红→checkout 还原→记录」。 - -### T3 画布自适应接线与断言(≈1d) - -**接口声明**——取用:无新接口(断言面;大图夹具自建 stub runner)。产出:无新接口(探针常数锚同源化)。 - -- [ ] `ui_input_probe.gd:98-99` 常数锚去手抄数字,改引 `level_map_screen.gd` 导出常量(`LAYER_BASE_X`/`LAYER_STEP_X`/`NODE_HALF`)——核查:探针绿且 `need_x = LAYER_BASE_X + maxl×LAYER_STEP_X + NODE_HALF.x + 8` 与屏内 `_fit_canvas` 公式逐项同源。 -- [ ] `level_map_info_fog_test.gd` 增大图夹具断言组:合成 11 层×6 槽含双 Boss 图(stub runner 供 `node_out_ids`/`node_id_of`),断言 `canvas_probe()["min_size"].x ≥ 70+最大层×122+NODE_HALF.x+8`、`min_size.y ≥ 52+最大槽×58+NODE_HALF.y+24`,且 `layout_probe` 全节点位置落在画布矩形内(routebug 83c11a1 同类回归)——核查:断言绿。 -- [ ] 变异验证:`_fit_canvas` 宽公式去 `NODE_HALF.x` 项 → 大图断言红 → `git checkout` 还原 → 记录。 - -### T4 冒烟补断言+变异汇总(≈1–1.5d) - -**接口声明**——取用:T1 `type_visible_ids()`、T2 `display_probe()`(真图消费面——补上探针的实机调用方)。产出:无新接口。 - -- [ ] `smoke_all.gd` `_smoke_level_graph` 段(:4552 起)末增真图可见性断言组(纯核心面,无需场景):L2 图入口停点 `type_visible_ids()` ⊇ `next_candidate_ids()`、boss id ∈ 集合、层距 > N 的非豁免 id ∉ 集合(N 取 `db.level.info_visible_layers`)——核查:冒烟绿。 -- [ ] `ui_input_probe.gd` 增 case⑱(图上可见性接线回归,挂在 ⑫ 图上命中之前的入口停点处):经 `ro.display_probe()` 断言真图雾下节点 draw_icon=false、Boss 节点 boss_border=true、候选节点 draw_icon=true;并以 push_input 点击一个雾下非候选节点,断言零提交(选路态不变——雾不扩大可点域,候选集即合法集纪律不破)——核查:探针绿。 -- [ ] T1–T3 变异验证记录表成文(每条新断言:注入实验/跑的闸/红输出/还原结论)进实现报告——核查:记录表覆盖全部新增断言、零「未证」条目。 -- [ ] 里程碑步出口跑一次代码域闸全套(冒烟+单元+探针+渲染/静态)+改前改后读数对拍——核查:全绿+对拍记录在案;**渲染闸含 shot_probe level 屏截图+shot_checker A-D 判据(屏级骨架/文字对比度/明度三档带),雾显隐后仍须全绿**;卡内修复只做出红最小单元的最窄确认。 - -### T5 产出物落位卡(≈0.5d,末卡——三份 `reports/` 文件写齐+独立验收审计通过才算收口) - -- [ ] `reports/iter-实现报告-<完成日>.md`:各卡状态+变异验证记录表+**实际改动—验收对照表**(§7.1 需求侧表的执行对账,另加「结果」格)+**实机目检总清单**(观测项三条:雾下可读性观感/多终点图 Boss 标记观感/大图完整可见观感——登记不阻塞,验收时间由用户自行安排)+遗留读数义务。 -- [ ] `reports/iter-代拍板追认单-<完成日>.md`:§9 白名单内代拍项留痕(provisional N=2、API 命名等)。 -- [ ] `reports/iter-反哺点-<完成日>.md`:§5 每条一落点(含呈报项「雾的正式视觉规格」+「超预算图策略」升级路径)。 -- [ ] 合并前置:独立验收审计(§7.1 末段声明)完成且报告 `reports/iter-验收审计-<完成日>.md` 逐行「通过」——未通过不得合并,缺漏项回炉对应 T 卡。 - -**估时合计 ≈4.5–6d**。切分预案:T1/T2 可切(核心出口先落、呈现接线后落);T3 独立可切;T4/T5 须连跑——建议单批跑完。 - -## 7. 验证闸(逐需求点验收清单) - -| # | 需求点 | 测什么(断言内容) | 落点 | 变异注入实验 | -|---|---|---|---|---| -| V1 | 邻近 N 层见类型 | 层距 ≤ N 的 id ∈ `type_visible_ids()`、层距 N+1 的非豁免 id ∉;入口停点锚层 0 | `level_map_type_vis_test` ①⑤ | 判定 `<=N` 改 ` 各新增断言 desc 统一尾缀〔fv#〕(本表「验收测试」列可 grep 定位);撤销/变异证明逐条对应 §7 变异注入实验栏与 T1–T4 变异步(每张任务卡至少一条,test-conventions §9.4)。 - -| 需求点 | 改动(文件:行/行为) | 验收测试(名称+文件:行+尾缀) | 层级 | 真实入口 | 撤销或变异证明 | -|---|---|---|---|---|---| -| fv1 邻近 N 层见类型 | `game/core/level_runner.gd` 新增 `type_visible_ids()`(层距判定) | type_vis ①层距边界+⑤入口锚/单链退化〔fv1〕 `tests/unit/core/level_map_type_vis_test.gd` | 单元 | `LevelRunner.type_visible_ids()` 直调 | 变异:`<=N` 改 `-验收审计-<完成日>.md`(逐行「通过/缺漏/例外及依据」)。**审计未通过不得合并**(T5 合并前置)。 - -## 8. 异常分级 - -- **L1(自救)**:断言写法、命名、探针字段名、测试夹具规模微调。 -- **L2(记录继续)**:雾下编号保留/标签行占位的绘制细节、label_text 措辞微调(4 字上限内)、单测夹具的图形态选择。 -- **L3(停机呈报)**:任何触碰冻结视觉件的诉求(雾画法/灰化遮罩/缩放/滚动/线宽/色值/形制);N < 1 或改调参表语义;候选被雾遮(决策锚破);把生成器/行进/敌人表改动混入本卡;`type_visible_ids` 需要持久化状态(违反纯函数口径);等比缩放/滚动被要求时须用户裁(契约 6)。 - -## 9. 代拍板白名单(超清单必呈报) - -1. provisional N=**2**(约束 int ≥ 1;依据:N≥1 是 GDD 硬下限,2=越过下一跳再看一层供岔路两跳规划,再多雾语义弱化;定值归定值批)。 -2. API/探针命名(`info_visible_layers`/`type_visible_ids`/`node_display`/`display_probe`)——约束:**禁 `fog_` 前缀**(与战斗棋盘「视野迷雾」撞词,code-conventions §2.2 跨层标识符语义单一)。 -3. 雾下节点编号保留、类型文字隐藏的具体落点写法(约束:结构通道零改动)。 -4. 大图断言夹具规模(约束:≥ 10 层 × ≥ 5 槽、含双 Boss、含跨层跳边)。 -5. `ui_input_probe` 常数锚同源化方式(内联引用 vs preload 常量——约束:两侧同源可核查、禁再手抄数字)。 -6. 提交切分与 worktree 分支命名。 - -## 10. 风险与异常处置(已知风险,不许绕过) - -⚠️ **风险 1 · level-map 视觉终稿已冻结(2026-09-28 样张 v1 闸形 A)**:雾的**画法**(视觉规格)动冻结件须用户裁。本卡只做可见性语义的最小呈现(沿用现役明暗/显隐语言,契约 4),「雾的正式视觉规格」(雾纹理/灰化遮罩/明度层)列进 §5 同步面+T5 呈报项,**禁自行重开冻结件**。若实现期发现显隐语义在实机上不可读,登记进实机目检清单呈报,禁临时加视觉件。 - -⚠️ **风险 2 · 图超屏预算**:完整可见 AC(level-map.md:197)冻结,若卡①生成图超出公式包络屏预算(1280×800 下约 10 层 × 8 槽),参数限图是不动冻结件的唯一兑现路径(契约 6);若定值批需要更大图,等比缩放/滚动视口两案均动冻结件 → 走呈报由用户裁(T5 反哺点登记升级路径)。 - -⚠️ **风险 3 · 术语撞词**:「信息迷雾」(本卡,关卡节点图类型可见性)vs「视野迷雾」(战斗棋盘,board_view/board_cell_unit 在役)——代码命名禁 `fog_` 前缀(代拍板 2),文档侧入术语表反哺点。 - -## 11. 待裁点 - -**零待裁点,开工门 = U0。** - -灰色地带均已在卡内定值并写明依据,经 grilling 设计树对抗复核后维持: - -| # | 灰色地带 | 定值 | 依据(可推导) | 淘汰案 | -|---|---|---|---|---| -| G1 | 「邻近 N 层」度量 | \|入口 BFS 层(node) − 层(锚)\| ≤ N | 调参表量纲「可见距离(层)」+地图以层列组织(LAYER_STEP_X=层间步距) | 跳数距离(量纲不符);node_depth 差(node_depth=寻址 id 非布局层,L2 图 node 7 深度 7/层 3) | -| G2 | 候选可见性 | 候选集强制豁免 | 生成.md:83「下一跳必在可见范围,是决策的可见锚」是语义承诺,直接兑现 | 依赖图形态保证(层跳边反例可破坏) | -| G3 | 已过节点回雾 | 单圈、零记忆 | GDD 三层表无「已见例外」+「单圈雾」口径明示可见性只依赖当前位置;已过节点不可再达(无回头路),回雾零决策损失 | 已见恒显(累积揭示,非单圈语义) | -| G4 | 显隐 or 灰化 | 显隐(类型图标+类型文字不绘制) | 用户范围注「沿用现役明暗/显隐语言」+「不见类型」语义=类型通道不出现;灰化仍泄类型形 | 灰化遮罩(属雾的正式视觉规格=冻结件域,呈报项) | -| G5 | N 落位与取值 | LevelTable 表级字段,provisional 2 | GDD「本系统自有参数一项」单参数+AdventureTable.clear_heal_pct 表级先例;2=越过下一跳一层前瞻 | 逐关行字段(无逐关设计需求);硬编码(违反改表即改玩法) | -| G6 | 超屏图策略 | 参数限图(生成参数档背书包络 ≤ 屏预算) | 完整可见 AC 冻结+「禁自行重开冻结件」⇒ 不动冻结件的唯一路径 | 等比缩放(动「20px 烘焙 1:1」「2/3/4px 折线」冻结值);滚动视口(新交互范式)——两案均须用户裁,列呈报升级路径 | -| G7 | 多终点/多入口校验放宽与层语义归属 | 归卡①(生成域规则语义);本卡层口径按现役唯一入口实现(level_table.gd structural 拦截第二无入边节点),多入口层语义随卡①产出规格定、经反哺点随动 | 关卡节点地图.md:14「图合法性规则语义以关卡节点图生成产出规格为准」;本卡呈现面已多 Boss 通用(F7);现役数据与校验均单入口 | 本卡顺带放宽校验或预设多入口层算法(越界卡①域,违反契约 8) | -| G8 | Boss 标记口径(F9 文档冲突) | 实现随现役代码+level-map.md(紫赤+深墨环) | 视觉终稿冻结在案(样张 v1)=实况权威;文档批核对走反哺点 | 按关卡节点地图.md:67「封印黑」改色(动冻结件且与样张冲突) | diff --git a/reports/需求卡片-敌人表深度带换代批-2026-10-21.md b/reports/需求卡片-敌人表深度带换代批-2026-10-21.md deleted file mode 100644 index 9d9c63ca..00000000 --- a/reports/需求卡片-敌人表深度带换代批-2026-10-21.md +++ /dev/null @@ -1,313 +0,0 @@ -# 需求卡片:敌人表深度带换代批(坐标寻址 → 深度带寻址)· 关卡节点图重构卡③ - -- 版本:**v1.0**(2026-10-21 成文;自审五查+grilling 复核结论见 §11) -- 来源:[待办清单](../待办清单.md)「关卡节点图重构——可选节点太少」(2026-10-13 登记,来源=playnote,设计面=文档轨,状态=文档设计已闭合)(唯一事实源:[关卡节点图生成](../design/gdd/systems/冒险循环/关卡节点图生成.md) v1.1+[关卡节点地图](../design/gdd/systems/冒险循环/关卡节点地图.md) v2.1,2026-10-21 双定稿闭合凭据) -- 归属:敌人系统数据面与遭遇寻址链(`game/data/schemas/enemy_table.gd`+`game/data/tables/enemy.tres`+`game/core/config_db_resolver.gd`);关卡节点图重构批卡③(卡①生成器/卡②地图行进/卡④迷雾同批) -- 排期:未编入(2026-10-18 用户令:需求卡片不预占迭代号,批次归属按工作量另裁) -- 开工门:U0(用户 `/iter` 发令)+ grilling 复核门过(零待裁点,见 §11) - -## 0. 前置依赖与并行性 - -- **设计闭合(第〇步)**:06 条目「关卡节点图重构——可选节点太少」状态=文档设计已闭合,凭据=关卡节点图生成.md v1.1+关卡节点地图.md v2.1(各带路径+版本号)→ 放行写卡。 -- 本卡**不依赖**卡①(生成器)/卡②(地图行进)的实现:迁移期带切法=旧 ceil 等价切法(§4 契约 2),对现行 `level.tres` 模板逐位零行为变化,可独立交付独立验收。 -- 本卡**向卡① 输出**推论条款 A(多终点深度互异,§4 契约 6)——「Boss 行按终点配置、多终点各配一行(走哪条路决定打谁)」在定位信息只有两样时的前提条件,归卡①产出规格兑现,落文走反哺点(§6 ③)。 -- 与卡② 的接口零改动:定位信息契约=(关卡序号 + 节点深度) 两样(关卡节点地图.md:119),本卡只换消费侧寻址,不动传递侧。 - -## 1. 目标与完成定义 - -**目标**:遭遇寻址从「(关卡序号 × 节点深度) 点坐标+向上取整兜底」换代为「(关卡序号 × 深度带) 带寻址」——深度带=节点深度的区间,配池按带不按点,难度坡随带递增;Boss 行按终点配置、多终点各配行集(走哪条路打谁)。敌人表 20 行数据随寻址同步换键,行集构成纪律与加载期校验清单随带寻址换代表述。 - -**相对现状,新增了什么**: -1. 表键换代:`(level_index, node_depth, enemy_id)` → `(level_index, row_kind, depth_band, enemy_id)`——`depth_band`(深度带键)替代 `node_depth`,新列 `row_kind ∈ {"battle","boss"}` 承载「Boss 行按终点配置」; -2. 带寻址单点:`EnemyTable.band_of`(带切法静态纯函数)+`ConfigDbResolver.resolve_enemy_rowset(L, D, row_kind)` 签名换代; -3. 多 Boss 各配行集的 schema 与校验(同关 Boss 带键互异、每关 Boss 行集 ≥1、跨表校验多终点带互异); -4. 首遇教学位的带下定位方式(首遇带=该敌种该关最低带;首遇行固定数量不加骰); -5. 冒烟断言面与单测面的坐标换代(§8 S 清单)。 - -**完成定义**(全部满足才算本批完成): -1. `enemy.tres` 20 行按 §4.2 迁移表完成键迁移,且逐行逐字段与迁移前对拍相等(数值零变更,数值只在 `data/tables`); -2. `EnemyTable.validate()` 对迁移后生产表零错误;加载期拒绝清单换代后旧类别不失守(§4 契约 7); -3. 带寻址单点成立:全仓遭遇行集解析只经 `resolve_enemy_rowset(L, D, row_kind)`,带切法只有 `EnemyTable.band_of` 一份实现; -4. 生产路径行为零变化:现行三关模板任意节点开战,生成行集与迁移前逐位一致(同种子同遭遇逐位复现由既有回放面承接); -5. 多 Boss 行集能力就位(schema+校验+跨表互异断言绿),且三关现有单终点 Boss 行迁移后仍各就其位; -6. 首遇教学位带下定位断言绿:L2 巨蜘蛛首遇带行集={giant_spider} 恰一行、`unit_count=2` 且无 `unit_count_max`(固定 2 只不加骰逐字保持); -7. §8 冒烟断言面随动清单逐条换代完毕、全绿;变异验证记录表进实现报告; -8. 批末重闸按改动域选档全绿+改前改后读数对拍;三份 `reports/` 产出物写齐(T4)。 - -## 2. 设计事实源 - -| # | 出处 | 取用条款 | -|---|---|---| -| D1 | `design/gdd/systems/冒险循环/关卡节点图生成.md` v1.1(2026-10-21,06 闭合凭据之一) | 依赖与输出表敌人系统行(:96):「战斗节点遭遇按深度带配池,遭遇坐标=关卡序号+深度带;多终点各配 Boss 行」;内容归 owner(:86)「战斗节点打谁=敌人系统按深度带配池」 | -| D2 | `design/gdd/systems/冒险循环/关卡节点地图.md` v2.1(2026-10-21,06 闭合凭据之一) | :65「遭遇坐标=关卡序号+节点深度,敌人表按**深度带**(深度区间)配池」+「终点恒在各路径最深处」;:119「定位信息只有两样:关卡序号 + 节点深度。战斗侧按深度带配池查表」 | -| D3 | `design/gdd/systems/战斗循环/敌人系统.md`(状态:已定稿) | 开战生成节(:67)「遭遇坐标(关卡序号 + 深度带——节点深度所在的区间)」;行集构成纪律行(:74)「敌人表按(关卡序号 × 深度带)存放;深度带=节点深度的区间——逐局生成使单点深度不再固定,配池按带不按点,难度坡随带递增……Boss 行按终点配置,多终点各配一行(走哪条路决定打谁)。纪律三条……」;调参表敌人表行(:229);hobgoblin 缺口登记(:81);验收标准(:269/:278) | - -闭合凭据一致性:D1/D2 与 06 条目状态内列的两份定稿逐字一致;D3 为敌人面语义源(开战生成+行集构成纪律),与 D1/D2 同向。 - -## 3. 现状事实(2026-10-21 worktree 实查;开工日 T0 复核行号) - -| # | 事实 | 出处 | -|---|---|---| -| F1 | 敌人表按 (level_index, node_depth, enemy_id) 逐坐标存放、行集可多行混编——schema 自述「逐 (level_index, node_depth, enemy_id) 行集的类型化读取」+「行序即契约:同坐标多行必须文件内相邻」 | `game/data/schemas/enemy_table.gd:2-3`;首行键实样 `game/data/tables/enemy.tres:7-10` | -| F2 | 按坐标取行集的代码=`ConfigDbResolver.resolve_enemy_rowset(L, D)`(ceil 语义:命中坐标返回全部行、按表文件行序;无精确键则向上取整保证偏难、越顶 clamp 取最大键+警告四元组去重);门面与消费面三处 | `game/core/config_db_resolver.gd:33-37`/:54-110(`_resolve_progress`)/:114-118(`_ceil_key`);门面 `game/core/config_db.gd:193-194`;消费面 `game/core/unit_container.gd:43`、`game/core/battle_orchestrator.gd:327`、`game/presentation/battle_narrator.gd:76-78` | -| F3 | 敌人表现状 20 行=L1 五坐标((1,0)goblin/(1,1)goblin/(1,2)bugbear/(1,3)hobgoblin/(1,4)ogre_chief Boss)+L2 八行七坐标((2,0)(2,1)bugbear/(2,4)giant_spider/(2,5)bugbear+giant_spider 混编双行/(2,6)(2,7)giant_spider/(2,9)drider Boss)+L3 七坐标((3,0)duergar/(3,1)(3,4)(3,6)hook_horror/(3,5)(3,7)duergar/(3,9)duergar_king Boss);Boss 行每关恰一行、坐标=各关最深节点 | `game/data/tables/enemy.tres:7-505`(行起点 :8/:29/:50/:73/:97/:124/:147/:171/:194/:217/:244/:271/:298/:328/:354/:377/:401/:427/:451/:478) | -| F4 | hobgoblin 缺口行:(1,3)「大地精」行在表在役(unit_count 2 / unit_count_max 3 / hp 14),缺口登记=「无立绘、不在本敌种表——补齐归敌种内容批(立绘+语义模板+敌种表行三件套)」 | `game/data/tables/enemy.tres:73-96`;`design/gdd/systems/战斗循环/敌人系统.md:81` | -| F5 | 首遇教学位读表与断言面:L2 巨蜘蛛 (2,4) 行 `unit_count=2` 且无 `unit_count_max`(不加骰由「行有 unit_count_max 才掷骰」机制承载);GDD 定位=「L2 首遇教学位(固定 2 只——首遇位不加骰)」;断言三处=L1 D2 熊地精固定 1、(2,4) 巨蜘蛛恰一行固定 2、(3,0) 灰矮人固定 1 | 行值 `enemy.tres:171-192`;GDD `敌人系统.md:88`;机制 `game/core/battle_orchestrator.gd:316-346`(有 unit_count_max 才入 encounter 流掷骰);断言 `game/smoke/smoke_all.gd:3406-3411`/:4692-4697/:4747-4752 | -| F6 | 混编行集与行序契约:(2,5) 双行(bugbear 重击手→giant_spider 巢群);同坐标多行必须文件内相邻(加载期 `rowset_rule` 拦截);生成序=表文件行序(逐行实例化);叙述取首行(boss_name 取 rowset[0]);行集实例总数 ≤4(`ROWSET_INSTANCE_MAX`,spawn 期守卫) | 行值 `enemy.tres:194-241`;校验 `enemy_table.gd:121-130`;生成 `game/core/unit_container.gd:85-120`;叙述 `battle_narrator.gd:68-79`;守卫 `enemy_table.gd:29`+`unit_container.gd:81-84` | -| F7 | 表校验加载期拒绝清单(`EnemyTable.validate()` 本表侧)=empty_table/dup_key(键含 enemy_id)/domain(L≥1、D≥0、unit_count、hp、atk、pool 非空、权重、armor_class)/cross_field(unit_count_max 区间、atk_min/max 成对+中点恒等)/enum(armor_type、attack_type)/rowset_rule(同坐标相邻)/anchor(每关 depth-0 锚行);跨表侧=④ 战斗/Boss 节点 (L,D) 在 board_gen 与 enemy 双表精确行(缺行加载致命)+⑤ 技能池悬空/池构成纪律 | `enemy_table.gd:32-135`;`game/core/config_db_cross_validator.gd:54-85`(④)/:86-90+:152-190(⑤) | -| F8 | 定位信息契约=(关卡序号 + 节点深度) 两样(图侧组装移交战斗结算);enc 已随行携带 `node_type`(battle/boss/…),现仅消费于变异池门控与叙述,未参与敌人行集寻址 | `design/gdd/systems/冒险循环/关卡节点地图.md:23/:111/:119`;`game/core/level_runner.gd:467-482`(begin_battle 组装) | -| F9 | 既有解析单点纪律在案:「进度键解析单点收口(全仓统走 resolve_enemy_rowset ceil 版……禁双算法并存〔状态记录 312〕)」 | `enemy_table.gd:138-140` | -| F10 | 改前读数基线=开工日 T0 采集(冒烟/单测通过数对拍面)——写卡会话不跑测(测试执行两入口铁规),本卡不预写数字 | — | - -## 4. 设计契约(红线,违反 = L3 停机呈报) - -🔴 **契约 1 · 寻址换代与表键**:遭遇寻址=(关卡序号, 节点深度) 入 → (关卡序号, row_kind, depth_band) 命中;敌人表键=`(level_index, row_kind, depth_band, enemy_id)`。`row_kind ∈ {"battle","boss"}`(对齐节点 type 词表),`depth_band`=深度带键(int ≥ 0,带=节点深度的区间)。行类列是「Boss 行按终点配置」的表侧表达:战斗节点与终点同深度时两行集不串位,同关多 Boss 各占自己的带键。 - -🔴 **契约 2 · 迁移期行为零变化**:迁移期带切法取**旧 ceil 等价切法**(带键=旧 node_depth 值、请求落带=旧 ceil 命中键、越顶 clamp 顶带+警告去重沿 R1)——生产路径(level_runner 组装的遭遇)迁移前后生成行集逐位一致。数值单一事实源:迁移后数值只进 `data/tables`,20 行数值字段逐字段零变更,禁任何字面量改写。 - -🔴 **契约 3 · 带切法单点**:带切法唯一实现=`EnemyTable.band_of(band_keys: Array, D: int) -> int`(静态纯函数,签名与算法见 §7 接口块);`ConfigDbResolver` 与 `ConfigDbCrossValidator` 共用它,禁双算法并存(F9 状态记录 312 同源口径)。带的区间划分(切法粗细)属实现细节,但两语义不可破:D<0 → fatal `request_oob`(沿用)、D 高于该 (L,row_kind) 顶带 → clamp 顶带+warning(类别沿用 `resolve_depth_clamp`、四元组去重沿用)。 - -🔴 **契约 4 · 行集构成纪律带键新表述**(三条纪律在带寻址下不破):① 行集内 enemy_id 唯一(同一敌种在同一 (L,row_kind,depth_band) 行集仍只许一行);② 同带键多行必须表文件内相邻,行序=声明序=生成序(行序即契约——resolver 返回序/生成序/叙述取首行全依赖文件行序);③ 行集实例总数(各行骰后数量求和)≤ 4,单行数量域 [1,3] 不变。违规加载期返回错误并止动。 - -🔴 **契约 5 · 多 Boss 各配行集**:Boss 行按终点配置、多终点各配一行集(行集可多行=Boss+护卫混编,既有 P4 乙口径不破;同行集双 Boss 敌种属合法混编形态,内容裁量归难度曲线);同关多 Boss 行集带键互异、禁两终点共带(共带=两终点命中同一 Boss 行集,「走哪条路决定打谁」落空)。Boss 行集每关 ≥1。表侧投影=每关 boss 行集 ≥1(加载期);图侧投影=同关多 boss 节点解析带键互异(跨表校验,T2)。 - -🔴 **契约 6 · 推论条款 A:多终点深度互异**:定位信息只有 (关卡序号 + 节点深度) 两样(D2:119)且「多终点各配一行(走哪条路决定打谁)」(D3:74)⇒ 终点身份只能由节点深度区分 ⇒ **多终点必须落在互异深度带**(同深度多终点在现行定位信息下不可区分,属设计不可能态)。推论依据=上述两条款的逻辑合取(无第三通道:扩定位信息=改 D2 契约,归卡②/文档批,不在本卡)。表侧投影=契约 5 带键互异校验;图侧兑现=卡①产出规格(落文走反哺点 §6 ③)。执行中发现同深度多终点图样 → L3 呈报。 - -🔴 **契约 7 · 加载期拒绝清单换代全兜底**:旧类别(empty_table/dup_key/domain/cross_field/enum/rowset_rule/anchor)一个不失守,键面随契约 1 换代;新增:row_kind 枚举封闭(非法值拒)、每关 Boss 行集 ≥1、(跨表侧)boss 节点带互异+节点深度禁顶 clamp 消解(D 高于该 (L,row_kind) 顶带键=加载致命,替代旧「精确行」严格性——带内命中是设计语义、顶 clamp 是调参事故,分界就在这条)。board_gen 半边精确行校验不动(§5)。 - -🔴 **契约 8 · 首遇教学位语义不破**:新敌种首遇带行固定数量不加骰(无 `unit_count_max`);L2 巨蜘蛛首遇带行 `unit_count=2` 逐字保持。首遇带定位方式=数据驱动:该 (L, battle, enemy_id) 的最低 `depth_band` 行集(不写死坐标数字)。 - -🔴 **契约 9 · hobgoblin 缺口行不静默处置**:数据去向=随迁带键、行值零变更、留任在役;缺口登记(无立绘/不在敌种表)随行携带,补齐三件套(立绘+语义模板+敌种表行)归敌种内容批。禁借迁移删行、改行或补立绘(内容面归敌种内容批,本卡只定数据去向)。 - -### 4.2 数据迁移规则(20 行去向,逐行可核) - -键迁移通则:`node_depth` 值照搬为 `depth_band` 值(迁移期带键=旧深度键,契约 2);`row_kind`="boss" 仅三行(旧坐标为 Boss 节点行者),其余 "battle";`enemy_id` 与全部数值/池字段逐字段零变更;文件内行序不变(行序即契约)。 - -| 旧键 (L, D, eid) | 新键 (L, row_kind, band, eid) | 备注 | -|---|---|---| -| (1,0,goblin) (1,1,goblin) (1,2,bugbear) | (1,battle,0/1/2,同 eid) | (1,2)=bugbear 首遇带行(固定 1 不加骰,F5) | -| (1,3,hobgoblin) | (1,battle,3,hobgoblin) | **hobgoblin 缺口行:留任随迁(契约 9)** | -| (1,4,ogre_chief) | (1,boss,4,ogre_chief) | L1 单终点 Boss 行 | -| (2,0,bugbear) (2,1,bugbear) | (2,battle,0/1,同 eid) | | -| (2,4,giant_spider) | (2,battle,4,giant_spider) | 巨蜘蛛首遇带行(固定 2 不加骰,契约 8) | -| (2,5,bugbear) (2,5,giant_spider) | (2,battle,5,同 eid) | 混编双行保持相邻、行序不变(契约 4②) | -| (2,6,giant_spider) (2,7,giant_spider) | (2,battle,6/7,同 eid) | | -| (2,9,drider) | (2,boss,9,drider) | L2 单终点 Boss 行(多终点时各配行集归后续内容投放) | -| (3,0,duergar) (3,1,hook_horror) | (3,battle,0/1,同 eid) | (3,0)=duergar 首遇带(固定 1);(3,1)=hook_horror 首遇带(固定 2) | -| (3,4,hook_horror) (3,5,duergar) (3,6,hook_horror) (3,7,duergar) | (3,battle,4/5/6/7,同 eid) | | -| (3,9,duergar_king) | (3,boss,9,duergar_king) | L3 单终点 Boss 行 | - -行数核对:battle 行 17+boss 行 3=20;迁移后 dup_key(新键)零命中、anchor(battle band 0)三关各一、boss 行集三关各一。 - -## 5. 范围边界 - -**做**:遭遇寻址换代(带切法单点+resolver 签名);敌人表 20 行键迁移(§4.2);表 schema 与加载期校验清单换代;行集构成纪律带键新表述;多 Boss 各配行集的 schema/校验能力(数据投放归后续内容面);首遇教学位带下定位;冒烟断言面与单测面随动(§8 S 清单);反哺点清单。 - -**不做**(明示去向): -- 生成器(节点图怎么长、多终点怎么切)→ **卡①**;推论条款 A 的产出规格落文 → 反哺点③,文档批落; -- 地图行进与定位信息传递((关卡序号+节点深度) 契约本体)→ **卡②**,本卡零改动; -- 迷雾与「走哪条=打谁」的呈现 → **卡④**; -- 敌种新内容:hobgoblin 补齐(立绘+语义模板+敌种表行)→ **敌种内容批**;本卡只定数据去向(契约 9),不补立绘不删行; -- `board_gen.tres`/`difficulty.tres` 的键带化(两表仍按 (level_index, node_depth) 点键+既有解析语义,`config_db_cross_validator.gd:54-85` 的 board_gen 半边精确行校验不动)——生成图深度扩面后两表的覆盖问题归卡①/后续表批登记,本卡不扩面; -- 行值投值与难度坡数值(池权重/hp/atk/AC)→ 难度曲线与Boss(数值单一事实源=`data/tables`,本卡零数值); -- 带切法未来粗化(带合并/区间调整)的具体切法——实现细节(契约 3 两语义不破即可),本卡迁移期钉旧 ceil 等价切法。 - -## 6. 关联文档同步面 - -> 本批是代码实现批,禁写 `design/` 整目录与根规则文件,唯一可写目录=`reports/`。下表「同步义务」一律执行为:逐条列进 `reports/iter-反哺点-<完成日>.md`(哪个文件哪一节、改成什么、依据),用户会话裁决后归文档批落文。 - -| 文档 | 关联点(本批实现会改变它的哪条陈述) | 同步义务 | -|---|---|---| -| `design/gdd/systems/战斗循环/敌人系统.md` | :81 hobgoblin 缺口登记的坐标记法「(1,3),即第一关第四节点」→ 带键记法「(1, battle, 3)」(数据留任语义不变);:278 表校验行「行集内同敌种第二行 / 同坐标多行不相邻」→「同带键多行不相邻」措辞随键换代 | 反哺点 · 列清单(①记法与措辞换代,依据=本卡契约 1/4) | -| `design/gdd/systems/冒险循环/关卡节点地图.md` | :146「本文递出关卡序号 + 节点编号,按编号生成单位归它」与 :119「关卡序号 + 节点深度……按深度带配池」两处表述不一致 → 统一为「关卡序号+节点深度;敌人侧按深度带配池生成单位」 | 反哺点 · 列清单(②用词统一,依据=:65/:119 既有定稿句) | -| `design/gdd/systems/冒险循环/关卡节点图生成.md` | 产出规格缺「多终点深度互异」条款——本卡推论条款 A(契约 6)的图侧兑现处 | 反哺点 · 列清单(③产出规格补条款:多终点落在互异深度带;依据=D2:119+D3:74 合取) | -| `待办清单.md`(06 登记处) | 本卡登记条状态推进「已写卡(`reports/需求卡片-敌人表深度带换代批-2026-10-21.md`)」(req-card 06 状态回写义务);实现批完成后按退役规则处理 | 反哺点 · 列清单(④状态回写与迭代行编入、条目退役记载——本会话文件面限于本卡一个文件,回写由批次收口/用户发令执行,防四卡并行互踩) | -| `roadmap.md` | 迭代行编入笔与批内状态刷新 | 反哺点 · 列清单(⑤) | -| `game/data/schemas/enemy_table.gd`、`game/data/tables/enemy.tres`、`game/core/config_db_resolver.gd`、`game/core/config_db.gd`、`game/core/unit_container.gd`、`game/core/battle_orchestrator.gd`、`game/core/config_db_cross_validator.gd`、`game/presentation/battle_narrator.gd` | 寻址与表承载改造本体 | 实施本体(可写) | -| `game/smoke/*`、`game/tests/*` | 断言面随动(§8 S 清单) | 实施本体(可写) | - -## 7. 任务卡 - -> 估时按 Claude 工作日口径;每卡出口自跑轻闸;开工日 T0 复核本卡全部行号。回归措辞模板沿用:里程碑步(T 段)出口跑一次全套闸;波内修复只做出红最小单元的最窄确认;新增测试先行孤立验证再并入全套。 - -### 卡前 · 文件结构图 - -| 文件 | 职责 | -|---|---| -| `game/data/schemas/enemy_table.gd` | 敌人表 schema:键换代+加载期拒绝清单换代+带切法单点 `band_of` | -| `game/data/tables/enemy.tres` | 敌人表数据:20 行键迁移+表尾迁移登记注释 | -| `game/core/config_db_resolver.gd` | 带寻址解析(L 维沿旧 ceil/clamp、D 维走 `band_of`) | -| `game/core/config_db.gd` | 门面 `resolve_enemy_rowset` 签名随动 | -| `game/core/unit_container.gd` | `spawn` 增 `row_kind` 入参、内部寻址随动 | -| `game/core/battle_orchestrator.gd` | 遭遇骰与 spawn 调用面随动(enc.node_type → row_kind) | -| `game/core/config_db_cross_validator.gd` | ④ 敌侧半边带化+Boss 带互异+顶 clamp 禁消解;board_gen 半边不动 | -| `game/presentation/battle_narrator.gd` | `boss_name` 行集寻址随动 | -| `game/smoke/smoke_all.gd`、`game/smoke/skill_rhythm_probe.gd`、`game/smoke/damage_probe.gd` | 断言面与直呼寻址面随动(§8 S 清单) | -| `game/tests/unit/core/config_db_resolver_test.gd`、`game/tests/unit/data_config/config_db_test.gd`、`game/tests/unit/core/config_db_cross_validator_test.gd` | 带寻址语义单测、夹具键改名、Boss 互异负向用例 | - -### 接口块(全卡共用,后卡引用以此为准) - -- **`EnemyTable.band_of(band_keys: Array, D: int) -> int`**(静态纯函数,带切法单点):`band_keys`=该 (level_index, row_kind) 的 depth_band 键升序数组;返回首个 ≥ D 的键;D 高于全部键 → 返回 -1(resolver 侧 clamp 顶带+warning,校验侧判「顶 clamp 消解」违例);`band_keys` 空 → 返回 -1 且 resolver 侧返回空行集(交消费侧既有「行集解析为空」fatal 路径,`battle_orchestrator.gd:328-330`)。算法体仅此一处(禁第二实现)。 -- **`EnemyTable.ROW_KINDS := ["battle", "boss"]`**(封闭枚举常量)。 -- **`ConfigDbResolver.resolve_enemy_rowset(L: int, D: int, row_kind: String) -> Array[Dictionary]`**:L<1 或 D<0 → `_diag` fatal `request_oob`(沿用);`row_kind` 非法 → `_diag` fatal(类别 `request_oob`);L 维 ceil/clamp 语义沿 `_resolve_progress` 现状;D 维=`band_of` 命中该 (L,row_kind) 带键行集、按表文件行序返回;`band_of` 返回 -1 → 取该 (L,row_kind) 顶带键+warning `resolve_depth_clamp`(四元组去重沿用)。 -- **`ConfigDb.resolve_enemy_rowset(L: int, D: int, row_kind: String) -> Array[Dictionary]`**(门面透传)。 -- **`UnitContainer.spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_overrides: Array = [], atk_override: int = -1, max_hp_override: int = -1, enemy_atk_overrides: Array = [], hero_attack_type: String = "", hero_armor_type: String = "", row_kind: String = "battle") -> bool`**(尾参新增,缺省 "battle")。 -- **`ConfigDbCrossValidator.cross_validate(loaded: Dictionary, push_diag: Callable) -> void`**(签名不变;④ 敌侧判据换代)。 - -### T0 事实坐实与迁移表核对(≈0.5–1d) - -- [ ] 复核 §3 全部 file:line 与 §4.2 迁移表 20 行(逐行对照 `enemy.tres` 实表:键、eid、数值),漂移即订正本卡——可核查结果:订正记录或「零漂移」声明进实现报告素材。 -- [ ] 改前读数基线采集(冒烟/单测通过数,两入口铁规下随 T1 迭代 loop 首跑取值)——可核查结果:基线读数记录在案。 -- [ ] 推论条款 A 与 §6 反哺点点①②③ 条目起草(一节一改一依据)——可核查结果:反哺点草稿段落在 T4 产物成文前就绪。 - -### T1 表 schema 换代+数据迁移+带寻址单点(≈1.5–2d) - -- [ ] 实装 `EnemyTable.ROW_KINDS := ["battle", "boss"]` 封闭枚举常量(落点 `enemy_table.gd`)。可核查结果:常量在位且被 validate 判据引用。 -- [ ] 实装 `EnemyTable.band_of`(签名与语义=接口块;落点 `enemy_table.gd`)。可核查结果:`band_of` 全仓唯一实现(全仓检索零第二份带切法)。 -- [ ] 实装 `validate()` 键换代(`enemy_table.gd:32-135`):dup_key 键串改 `"%d_%s_%d_%s" % [L, row_kind, depth_band, enemy_id]`;domain 增 `row_kind ∈ ROW_KINDS`、`depth_band ≥ 0`(原 D 域判据换代);rowset_rule 键改 `"%d_%s_%d" % [L, row_kind, depth_band]`(相邻语义不变);anchor 改「每关存在 row_kind="battle" 且 depth_band=0 行」+新增「每关存在 row_kind="boss" 行 ≥1」(错误类别沿 `anchor`);其余判据(unit_count/unit_count_max/hp/atk/pool/armor/attack_type/armor_class)零改动。可核查结果:`validate()` 对旧格式夹具报键类错误、对新格式合法夹具零错。 -- [ ] 实装 `enemy.tres` 20 行键迁移(严格按 §4.2 表:depth_band 值=旧 node_depth 值、三行 row_kind="boss"、其余 "battle"、数值/池字段零变更、行序不变)。可核查结果:迁移后 20 行新键与 §4.2 逐行对上,且 `git diff` 仅键列+新增 kind 列、值列零变更(件核=V3)。 -- [ ] 实装 `enemy.tres` 表尾迁移登记注释一段(日期+键换代说明+hobgoblin 留任口径)。可核查结果:注释在位且与 §4.2/契约 9 同口径。 -- [ ] 验收步骤:跑 `godot --headless --path game --script res://tests/unit/data_config/config_db_test.gd`(先按 V2 改造夹具)与冒烟表校验段,期望「EnemyTable validate 零错误」断言绿。 -- [ ] 实装 `ConfigDbResolver.resolve_enemy_rowset(L, D, row_kind)` 换代+`EnemyTable.band_of` 接线(`config_db_resolver.gd:33-37` 重写;`_resolve_progress` 保留给 board_gen 零改动);`config_db.gd:193-194` 门面签名随动。可核查结果:`resolve_enemy_rowset(1, 3, "battle")` 返回 hobgoblin 行集、`resolve_enemy_rowset(1, 4, "boss")` 返回 ogre_chief 行集(夹具面)。 -- [ ] 验收步骤:跑 `godot --headless --path game --script res://tests/unit/core/config_db_resolver_test.gd`(按 V1 换代后),期望带寻址语义五断言全绿(见 §8 V1)。 -- [ ] 变异验证(注入→最窄闸见红→`git checkout` 还原→记录):把 `band_of` 改成「返回首个 ≤ D 的键」(floor 反向)→ 期望 V1「带命中偏难」断言红;把 `validate` 的 row_kind 枚举判据注释掉 → 期望 V2「非法 row_kind 被拒」断言红。记录表进实现报告。 - -### T2 消费面与跨表校验随动(≈1–1.5d) - -- [ ] 实装 `unit_container.gd`:`spawn` 尾参增 `row_kind: String = "battle"`,内部改 `db.resolve_enemy_rowset(level_index, node_depth, row_kind)`(:43),行集为空的报错消息随键换代(:45)。可核查结果:既有 spawn 调用(缺省战斗)零改动可编译,传 "boss" 取 Boss 行集。 -- [ ] 实装 `battle_orchestrator.gd`:`begin_battle` 内取 `var rk := String(enc.get("node_type", "battle"))`(缺省 battle=§10 代拍板③),`erowset_pre`(:327)与 `_enemy_system.spawn`(:355)传 rk。可核查结果:enc 带 `node_type:"boss"` 时遭遇行集为 Boss 行集,缺省时为 battle 行集。 -- [ ] 实装 `battle_narrator.gd` `boss_name`(:76-78):寻址改 `db.resolve_enemy_rowset(int(runner.level_row.level_index), int(node.get("node_depth", -1)), "boss")`。可核查结果:Boss 节点叙述名取 Boss 行集首行 display_name。 -- [ ] 实装 `config_db_cross_validator.gd` ④ 敌侧半边换代(:63-65、:81-85):battle/boss 节点 (L,D) 判据改「(L,row_kind) 行集非空且 `EnemyTable.band_of(该类带键, D) != -1`(-1=顶 clamp 消解,报 `level_node_dangling`)」;新增「同关多 boss 节点解析带键互异(同带报错,类别 `rowset_rule`)」;board_gen 半边(:62、:82-83)零改动。可核查结果:负向夹具(顶 clamp/同带双 boss)各报一行错误。 -- [ ] 验收步骤:跑 `godot --headless --path game --script res://tests/unit/core/config_db_cross_validator_test.gd`(按 V4 改造后),期望既有三用例+新增两负向用例全绿。 -- [ ] 验收步骤:T 段出口跑一次全套闸(`tools/run_all_gates.sh` 按改动域选档,本批改动域=`game/` 代码与数据),期望代码域全绿、既有单测(settle_segments 等零改动面)零红。 -- [ ] 变异验证:删掉「boss 节点带互异」判据 → 期望 V4 同带双 boss 负向用例红;把 ④ 敌侧 `band_of != -1` 判据短路为恒真 → 期望顶 clamp 负向用例红。记录表进实现报告。 - -### T3 冒烟断言面随动+首遇带定位(≈1.5–2d) - -- [ ] 按 §8 S 清单逐条换代 `smoke_all.gd` 直呼面(S1/S3/S4/S5/S16:`resolve_enemy_rowset` 三参化)——可核查结果:清单条目逐条勾销、冒烟该段绿。 -- [ ] 换代首遇教学位断言(S2/S9+契约 8 定位方式):改为数据驱动定位(扫表取该敌种该关最低 depth_band),断言=「L2 巨蜘蛛首遇带行集 = {giant_spider} 恰一行且 unit_count=2 且无 unit_count_max」+「L1 bugbear 首遇带行 unit_count=1 无 unit_count_max」+「L3 duergar 首遇带行 unit_count=1 无 unit_count_max」——可核查结果:三断言绿且不含写死坐标数字。 -- [ ] 换代混编与行序断言(S4):带键 (2,battle,5) 行集=双行、行序 [bugbear, giant_spider]、Σ(unit_count_max 缺省=unit_count) ≤ 4——可核查结果:断言绿。 -- [ ] 换代跨表覆盖与曲线断言(S6/S7):en_keys 键改 (L,row_kind,depth_band) 聚合、Boss 节点查 boss 键;L1 总量曲线断言改按 (1,battle) depth_band 升序相邻带总量比 ≤ 2.5(现值 12/10/22/35/72 口径不变)——可核查结果:断言绿。 -- [ ] 换代毒吐投放面断言(S8):判据改「pool 含 venom_spit 的行 ⇒ enemy_id=="giant_spider" 且 depth_band > 巨蜘蛛首遇带」+「巨蜘蛛首遇带行 pool 恒 {attack:6, bite:2, charge:1}」(去 `>=5` 写死)——可核查结果:断言绿。 -- [ ] 换代 Boss 坐标直呼面补 node_type(S10a:`smoke_all.gd:2022/:3025/:3090/:3177/:3231/:3271/:3301/:3474/:4503/:4933`、`damage_probe.gd:95` 各补 `"node_type": "boss"`)——可核查结果:各段绿、Boss 行集解析正确。 -- [ ] 换代 kind/坐标不一致直呼面(S10b:`smoke_all.gd:2621/:2646` 的 (2,4)+`node_type:"boss"` 改 (2,9)+`node_type:"boss"`——其断言语义「Boss 图回落/确定性」不变,棋盘载体改真 Boss 坐标)——可核查结果:该段绿、消息文本更新为新坐标。 -- [ ] 换代探针直呼面(S14:`skill_rhythm_probe.gd:17`、`damage_probe.gd:269/:420` 三参化)——可核查结果:探针可跑通。 -- [ ] 验收步骤:T 段出口跑一次全套闸,期望冒烟全绿、探针段无红。 -- [ ] 变异验证:给巨蜘蛛首遇带行加 `unit_count_max: 3` → 期望首遇「不加骰」断言红;把混编两行文件内对调 → 期望行序断言红;把 hobgoblin 行迁到错误带键 (1,battle,2) → 期望 V3 键位断言红。记录表进实现报告。 - -### T4 产出物落位卡(≈0.5d,末卡——三份 `reports/` 文件写齐+独立验收审计通过才算收口) - -- [ ] 轻闸全绿后、合并前写齐三份:`reports/iter-实现报告-<完成日>.md`(各卡状态+变异验证记录表+实机目检总清单+遗留读数义务)/`reports/iter-代拍板追认单-<完成日>.md`(§10 白名单内代拍项留痕)/`reports/iter-反哺点-<完成日>.md`(§6 五条每条一落点:①敌人系统.md 记法/措辞、②关卡节点地图.md :146 用词、③关卡节点图生成.md 推论条款 A、④06 状态回写与条目记载、⑤roadmap 编入笔)。可核查结果:三文件在 `reports/` 且 §6 同步面每条有落点。 -- [ ] 合并前置:独立验收审计(§8.2 末段声明)完成且报告 `reports/iter-验收审计-<完成日>.md` 逐行「通过」——未通过不得合并,缺漏项回炉对应 T 卡。可核查结果:审计报告在 `reports/` 且逐行「通过」。 - -**估时合计 ≈5–6.5d**(T0 0.5–1+T1 1.5–2+T2 1–1.5+T3 1.5–2+T4 0.5)。切分预案:T1/T2 后可切(表与解析先落、消费面后落)——但 T1 出口到 T2 之间编译面是断的(签名换代),建议 T1+T2 一轮同落、T3 单独一轮;超软盒(>7d)预挂:T3 冒烟面清单若超一孔,先落 S1–S5+S9+S10a/S10b(寻址三参化/首遇带/Boss 直呼主链)保主链绿,S6–S8+S14–S16 次轮补,全清单绿才收口。 - -## 8. 验证闸(逐需求点验收清单) - -| # | 需求点 | 测什么(断言/行为) | 落点/判定手段 | -|---|---|---|---| -| V1 | 带寻址语义 | ①带命中:请求 D 落带 i → 返回该带行集(夹具键 [0,5]:请求 4 → 命中带 5,偏难不回 0);②行序保序(行集按表文件行序返回);③顶 clamp:D 高于顶带 → 顶带行集+warning `resolve_depth_clamp` 一次(重复请求去重);④row_kind 路由:同 D 传 "battle"/"boss" 命中不同键域;⑤D<0/L<1 → fatal `request_oob` | 单测 `config_db_resolver_test.gd`(换代既有 `_test_ceil_progress_key`/`_test_rowset_order_preserved` 为带语义四断言+新增路由断言) | -| V2 | 表 schema 与校验换代 | 迁移后生产表 `validate()` 零错误;非法 row_kind 被拒;dup_key 新键拦截(同 (L,kind,band,eid) 二行报 dup);rowset_rule 同带隔断报错;anchor(battle band 0+boss ≥1)缺任一报错 | 冒烟表校验段+单测夹具(`config_db_test.gd`) | -| V3 | 数据迁移零变更 | 键位与结构:20 行计数、键序=§4.2 表序、hobgoblin 行在 (1,battle,3)、三 boss 行 kind/band 就位;**数值零变更不钉字面量**(AGENTS.md 数值单一事实源红线)——由 T1 迁移步骤「git diff 仅键列+新增 kind 列、值列零变更」件核承载 | 冒烟结构断言(键/计数/行序)+git diff 件核(T0/T1 清单) | -| V4 | 多 Boss 各配行集 | 同关两 boss 节点解析带键互异(同带 → 报错);节点深度触发顶 clamp → 报错;单终点现状三关零误报 | 单测 `config_db_cross_validator_test.gd` 新增两负向+既有三用例回归 | -| V5 | 行集构成纪律带键新表述 | 混编 (2,battle,5) 双行行序 [bugbear→giant_spider];Σ(unit_count_max 缺省=unit_count) ≤ 4;同带隔断加载拒 | 冒烟 S4+`enemy_table` 校验(V2 面) | -| V6 | 首遇教学位带下定位 | 首遇带=数据驱动最低带;L2 巨蜘蛛首遇带行集恰一行、unit_count=2、无 unit_count_max;L1 bugbear/L3 duergar 首遇带固定数同款 | 冒烟 S2/S9 换代断言 | -| V7 | 冒烟断言面随动完整 | §8.1 S 清单全部条目(S1–S16,S10 分 a/b 共 17 行)逐条换代、无一条留旧坐标语义 | 冒烟全绿+清单件核(实现报告逐条勾销) | -| V8 | hobgoblin 缺口行去向 | (1,battle,3,hobgoblin) 在役(V3 结构断言面)、缺口登记随行(不删不补、不加立绘);数值相等归 V3 git diff 件核 | 冒烟结构断言+件核(敌种内容批接续登记在反哺点①) | -| V9 | 反哺点清单 | §6 五条每条有落点 | 件核对表(T4) | - -### 8.1 冒烟断言面随动清单(S 清单——按旧坐标写就、须换代的断言/直呼面,2026-10-21 实查) - -| # | 断言/直呼面 | 位置 | 换代内容 | -|---|---|---|---| -| S1 | `resolve_enemy_rowset` 行集首行读数(遭遇扫描/D1 D2 掷骰段) | `game/smoke/smoke_all.gd:3115`/:3429/:4396 | 三参化(row_kind="battle"),带语义注释 | -| S2 | 巨蜘蛛首遇行集 (2,4) 定点断言 | `smoke_all.gd:4692-4697` | 首遇带数据驱动定位+「恰一行、unit_count=2、无 unit_count_max」 | -| S3 | L2 Boss 行集 (2,9) 断言 | `smoke_all.gd:4698-4702` | 三参化(row_kind="boss") | -| S4 | 混编行集 (2,5) 行序/骰和/行值断言 | `smoke_all.gd:4706-4719` | 键改 (2,battle,5);行序契约断言保持 | -| S5 | L3 首遇 (3,0) 与 Boss (3,9) 断言 | `smoke_all.gd:4747-4759` | 首遇带定位+三参化(boss) | -| S6 | 跨表覆盖义务 en_keys 键聚合 | `smoke_all.gd:971-981`/:4677-4687/:5144-5147 | 键改 (L,row_kind,depth_band);Boss 节点查 boss 键域 | -| S7 | Boss AC 台阶+(1,2) 锚定+L1 总量曲线 | `smoke_all.gd:2206-2244` | node_depth→depth_band;(1,2) 锚定改 bugbear 首遇带定位;曲线断言改按 (1,battle) 带键相邻比 ≤ 2.5 | -| S8 | 毒吐投放面(venom_spit 专行检查+池权重图) | `smoke_all.gd:4178-4230` | 「`node_depth >= 5`」改「depth_band > 巨蜘蛛首遇带」;池键串改含 row_kind | -| S9 | D2 熊地精固定 1(首遇教学位) | `smoke_all.gd:3406-3411` | (1,2) 改 bugbear 首遇带定位(固定 1 不加骰) | -| S10a | Boss 坐标 `begin_battle` 直呼缺 node_type | `smoke_all.gd:2022`/:3025/:3090/:3177/:3231/:3271/:3301/:3474/:4503/:4933;`smoke/damage_probe.gd:95` | 各补 `"node_type": "boss"` | -| S10b | kind/坐标不一致直呼((2,4)+boss) | `smoke_all.gd:2621`/:2646 | 改 (2,9)+boss(断言语义不变,载体改真 Boss 坐标) | -| S11 | resolver 单测(ceil 语义/行序保序) | `game/tests/unit/core/config_db_resolver_test.gd:51-70` | 换代为 V1 带寻址语义断言 | -| S12 | `resolve_enemy_rowset` 同通道单测+夹具键 | `game/tests/unit/data_config/config_db_test.gd:38-40`/:62-69/:201 | 夹具键改 row_kind/depth_band;签名三参化 | -| S13 | 跨表校验单测夹具 | `game/tests/unit/core/config_db_cross_validator_test.gd:54`/:66/:79 | 夹具键改名+补 V4 两负向用例 | -| S14 | 探针直呼面 | `game/smoke/skill_rhythm_probe.gd:17`;`game/smoke/damage_probe.gd:269`/:420 | 三参化 | -| S15 | `boss_name` 叙述寻址 | `game/presentation/battle_narrator.gd:76-78` | 三参化(row_kind="boss") | -| S16 | 全深度×多种子 spawn 扫描(erow2 首行行值读数) | `smoke_all.gd:3413-3433` | 首行 unit_count 读数随带键(三参化) | - -清单口径:S11–S13 归单测面(T1/T2),S1–S10、S14–S16 归冒烟/探针面(T3);S15 归 T2(消费面)。无第 17 类遗漏面——`tools/` 下 node_depth 引用属关卡模板历史脚本(`t5_build_templates.py` 等),非敌人表键,不随动。 - -**测试方式两分法(2026-10-11 用户定)**:V1–V9 全部机检自动化(冒烟+单测);能力验证可经调试接口直改敌人表/遭遇坐标直呼开战(沿用现役 DebugOverlay/DS_DEBUG 门控)。**用户目检项**:仅「三关实机开战遭遇观感不回归」一条——观测项写进 `reports/iter-实现报告-<完成日>.md` 实机目检总清单,登记不阻塞(验收时间用户自行安排)。 - -**闸口径**:轻闸每任务卡出口自跑;批末重闸=`tools/run_all_gates.sh` 按改动域选档(本批只碰 `game/` 代码与数据表 → 代码域档)全绿才合并+改前改后读数对拍。回归措辞:里程碑步(T 段)出口跑一次全套闸;波内修复只做出红最小单元的最窄确认;新增测试先行孤立验证再并入全套。 - -**变异验证伴生**(新增/改写验收函数逐条注入实验,记录表进实现报告):`band_of` 反向 floor(V1 红)/row_kind 枚举判据注释(V2 红)/boss 带互异判据删除(V4 红)/顶 clamp 判据短路(V4 红)/首遇行加 unit_count_max(V6 红)/混编行对调(V5 红)/hobgoblin 迁错带键(V3 红)。数值零变更不设数值字面量断言(数值单一事实源红线),改键误写由键位/行序/计数断言兜、值列漂移由 git diff 件核兜。变异不红则改判据或如实标注拦截边界。 - -### 8.2 改动—验收对照表(2026-10-21 验收闭合批新规,test-conventions §9.3) - -> 各断言 desc 统一尾缀〔fv#〕(本表「验收测试」列可 grep 定位);撤销/变异证明逐条对应 §8 变异验证伴生清单与 T1–T3 变异步(每张 T 卡至少一条,test-conventions §9.4)——T0 为事实坐实卡(无行为改动),其迁移表核对红证并入 V3 行(hobgoblin 迁错变异);T4 产出物以件核撤销证明承载。 - -| 需求点 | 改动(文件:行/行为) | 验收测试(名称+文件:行+尾缀) | 层级 | 真实入口 | 撤销或变异证明 | -|---|---|---|---|---|---| -| V1① 带命中偏难 | `game/core/config_db_resolver.gd:33-37` D 维改走带寻址+`game/data/schemas/enemy_table.gd` 新增 `band_of` 纯函数(接口块签名) | 带命中偏难(夹具键 [0,5]:请求 4 命中带 5、偏难不回 0)〔fv1〕 `game/tests/unit/core/config_db_resolver_test.gd`(S11 换代) | 单元 | `EnemyTable.band_of()`/`ConfigDbResolver.resolve_enemy_rowset()` 直调 | 变异:`band_of` 改「返回首个 ≤ D 的键」(T1 变异步)→ 本断言红 | -| V1② 行序保序 | `config_db_resolver.gd` 带命中行集按表文件行序返回(行序即契约) | 行序保序〔fv2〕 `config_db_resolver_test.gd`(S11 换代 `_test_rowset_order_preserved`) | 单元 | `resolve_enemy_rowset()` 直调 | 变异:混编两行文件内对调(T3 变异步)→ 行序断言红 | -| V1③ 顶 clamp+warning 去重 | `config_db_resolver.gd` `band_of` 返回 -1 → 顶带键行集+warning `resolve_depth_clamp`(四元组去重沿用) | 顶 clamp+warning 一次去重〔fv3〕 `config_db_resolver_test.gd` | 单元 | `resolve_enemy_rowset()` 直调(D 高于顶带、重复请求两次) | 撤销证明:回退 D 维 clamp 接线 hunk → 本断言红(§9.4 既有行为换代) | -| V1④ row_kind 路由 | `resolve_enemy_rowset(L, D, row_kind)` 三参签名(`config_db_resolver.gd:33-37`+`config_db.gd:193-194` 门面随动) | row_kind 路由:同 D 传 battle/boss 命中不同键域〔fv4〕 `config_db_resolver_test.gd` | 单元 | 同 D 分别传 "battle"/"boss" 直调 | 撤销证明:撤销第三参接线 hunk → 同 D 双 kind 不再分域、断言红 | -| V1⑤ request_oob | `config_db_resolver.gd` L<1 或 D<0 → `_diag` fatal `request_oob`(沿用) | D<0/L<1 fatal `request_oob`〔fv5〕 `config_db_resolver_test.gd` | 单元 | `resolve_enemy_rowset(0, …)`/`(L, -1, …)` 直调 | 撤销证明:回退 OOB 判据 hunk → 断言红 | -| V2① 生产表 validate 零错误 | `enemy_table.gd:32-135` validate 键换代+`game/data/tables/enemy.tres` 20 行键迁移 | 迁移后生产表 validate 零错误〔fv6〕 冒烟表校验段 `game/smoke/smoke_all.gd`+`config_db_test.gd` | 接口 | `EnemyTable.validate()` 对 `enemy.tres` | 撤销证明:回退键换代/迁移 hunk → validate 键类错误红 | -| V2② 非法 row_kind 被拒 | `enemy_table.gd` domain 判据增 `row_kind ∈ ROW_KINDS`(`ROW_KINDS := ["battle","boss"]`) | 非法 row_kind 被拒〔fv7〕 `game/tests/unit/data_config/config_db_test.gd`(S12 夹具) | 接口 | `validate()` 非法枚举夹具 | 变异:`validate` 的 row_kind 枚举判据注释掉(T1 变异步)→ 本断言红 | -| V2③ dup_key 新键拦截 | `enemy_table.gd` dup_key 键串改 `"%d_%s_%d_%s"`(L,row_kind,depth_band,enemy_id) | dup_key 新键拦截(同键二行报 dup)〔fv8〕 `config_db_test.gd` | 接口 | `validate()` 同键二行夹具 | 撤销证明:回退 dup_key 键串 hunk → 同键二行漏拦、断言红 | -| V2④ rowset_rule 同带隔断 | `enemy_table.gd:121-130` rowset_rule 键改 `"%d_%s_%d"`(同带相邻语义不变) | rowset_rule 同带隔断报错〔fv9〕 `config_db_test.gd` | 接口 | `validate()` 隔断夹具 | 撤销证明:回退 rowset_rule 键串 hunk → 隔断漏拦、断言红 | -| V2⑤ anchor 缺任一报错 | `enemy_table.gd` anchor 改「每关 battle band 0」+新增「每关 boss 行集 ≥1」(类别沿 `anchor`) | anchor(battle band 0+boss ≥1)缺任一报错〔fv10〕 `config_db_test.gd` | 接口 | `validate()` 缺锚/缺 boss 夹具 | 撤销证明:回退 anchor 判据 hunk → 缺任一漏报、断言红 | -| V3① 20 行计数与键序 | `enemy.tres` 20 行键迁移(T0 逐行复核 §4.2 迁移表+T1 迁移步,文件行序不变) | 20 行计数+键序=§4.2 表序〔fv11〕 冒烟结构断言 `smoke_all.gd` | 接口 | 加载 `enemy.tres` 全表(冒烟表校验段) | 撤销证明:回退 `enemy.tres` 迁移 hunk(T0 核对面+T1 迁移面)→ 计数/键序断言红 | -| V3② hobgoblin 键位 | (1,3,hobgoblin) → (1,battle,3,hobgoblin) 留任随迁(契约 9) | hobgoblin 行在 (1,battle,3)〔fv12〕 冒烟结构断言 `smoke_all.gd` | 接口 | 同 fv11 读表面 | 变异:hobgoblin 迁错带键 (1,battle,2)(T3 变异步)→ 键位断言红(红证覆盖 T0 §4.2 迁移表核对面) | -| V3③ 三 boss 行 kind/band 就位 | 三行 `row_kind="boss"` 就位((1,boss,4)ogre_chief/(2,boss,9)drider/(3,boss,9)duergar_king,§4.2 表) | 三 boss 行 kind/band 就位〔fv13〕 冒烟结构断言 `smoke_all.gd` | 接口 | 同 fv11 读表面 | 撤销证明:回退任一 boss 行 kind 列 hunk → 断言红 | -| V3④ 值列零变更件核 | `enemy.tres` 仅键列+新增 kind 列(T1 迁移步:值列零变更、禁字面量改写) | 值列零变更〔fv14〕 git diff 件核(T1 迁移步清单) | 件核 | `git diff game/data/tables/enemy.tres` 件核 | 撤销证明:改任一值列字面量(T1 件核步)→ git diff 件核红 | -| V4① 同带双 boss 报错 | `config_db_cross_validator.gd:63-65/:81-85` ④ 敌侧带化+「同关多 boss 节点解析带键互异」判据(类别 `rowset_rule`) | 同带双 boss 负向报错〔fv15〕 `game/tests/unit/core/config_db_cross_validator_test.gd`(S13 新增负向) | 单元 | `ConfigDbCrossValidator.cross_validate()` 同带双 boss 夹具 | 变异:删「boss 节点带互异」判据(T2 变异步)→ 本用例红 | -| V4② 顶 clamp 消解报错 | ④ 敌侧 `band_of != -1` 判据(-1=顶 clamp 消解,报 `level_node_dangling`) | 顶 clamp 负向报错〔fv16〕 `config_db_cross_validator_test.gd` | 单元 | `cross_validate()` 节点深度高于顶带夹具 | 变异:④ 敌侧判据短路为恒真(T2 变异步)→ 本用例红 | -| V4③ 单终点三关零误报 | ④ 敌侧半边换代、board_gen 半边零改动(:62/:82-83) | 既有三用例回归零误报〔fv17〕 `config_db_cross_validator_test.gd` 既有三用例(S13 夹具键改名) | 单元 | `cross_validate()` 现役三关夹具 | 撤销证明:回退 ④ 敌侧换代 hunk → 改名后既有三用例红 | -| V5① 混编行序 | (2,5) 双行迁 (2,battle,5)、行序 [bugbear→giant_spider] 与文件相邻不变(`enemy.tres`) | 混编 (2,battle,5) 双行行序〔fv18〕 冒烟 S4 `smoke_all.gd:4706-4719` 换代 | 集成 | 冒烟 S4 段读 (2,battle,5) 行集 | 变异:混编两行文件内对调(T3 变异步)→ 行序断言红 | -| V5② 骰和 ≤ 4 | 行集实例总数纪律带键化(`enemy_table.gd:29` `ROWSET_INSTANCE_MAX`+`unit_container.gd:81-84` 守卫) | Σ(unit_count_max 缺省=unit_count) ≤ 4〔fv19〕 冒烟 S4 | 集成 | 同 fv18 读行集面 | 撤销证明:放宽 ≤4 判据/守卫 hunk → 断言红 | -| V5③ 同带隔断加载拒 | 同 fv9 面(`enemy_table.gd:121-130` rowset_rule 带键化) | 同带隔断加载拒〔fv20〕 `config_db_test.gd`(V2④ 面合用) | 接口 | `validate()` 隔断夹具 | 撤销证明:同 fv9(回退 rowset_rule 键串 hunk → 断言红) | -| V6① 首遇带数据驱动定位 | T3 首遇断言定位换代为扫表取该 (L,battle,eid) 最低 depth_band(禁写死坐标,契约 8) | 首遇带=数据驱动最低带〔fv21〕 冒烟 S2/S9 `smoke_all.gd:4692-4697`/`:3406-3411` 换代 | 集成 | 冒烟 S2/S9 段扫 `enemy.tres` | 撤销证明:撤销扫表定位 hunk(写回写死坐标)→ 三断言取键失配红;件核零写死坐标(契约 8) | -| V6② L2 巨蜘蛛首遇带 | S2 换代:首遇带行集恰一行、`unit_count=2`、无 `unit_count_max`(S2 :4692-4697) | 巨蜘蛛首遇带恰一行固定 2 不加骰〔fv22〕 冒烟 S2 `smoke_all.gd:4692-4697` 换代 | 集成 | 同 fv21 扫表定位面 | 变异:巨蜘蛛首遇行加 `unit_count_max: 3`(T3 变异步)→「不加骰」断言红 | -| V6③ L1/L3 首遇带固定数 | S9/S5 换代:L1 bugbear 首遇带固定 1、L3 duergar 首遇带固定 1、均无 `unit_count_max` | bugbear/duergar 首遇带固定 1 不加骰〔fv23〕 冒烟 S9 `smoke_all.gd:3406-3411`+S5 `:4747-4759` 换代 | 集成 | 同 fv21 扫表定位面 | 变异:首遇行加 `unit_count_max`(T3 变异步同法注入 L1/L3 首遇行)→ 断言红 | -| V7 S 清单随动完整 | §8.1 S1–S16(17 行)逐条换代(`smoke_all.gd`/`skill_rhythm_probe.gd`/`damage_probe.gd`/单测三件) | S 清单 17 行无一留旧坐标语义〔fv24〕 清单件核(实现报告逐条勾销)+冒烟/探针全绿 | 单元+集成+探针 | 冒烟/探针/`tests/run_all` 全套入口 | 分证变异挂 fv1/fv2/fv7/fv18/fv22(T1–T3 变异步);整面撤销证明:任一 S 条留旧语义(撤销该 hunk)→ 对应断言红 | -| V8 hobgoblin 缺口行去向 | `enemy.tres` (1,battle,3,hobgoblin) 留任随迁、行值零变更、缺口登记随行(契约 9:不删行不补立绘) | hobgoblin 在役+缺口登记随行〔fv25〕 冒烟结构断言(V3 面)+git diff 件核 | 接口 | 同 fv11 读表面+`git diff` 件核 | 变异:hobgoblin 迁错带键(T3 变异步,同 fv12)→ 键位断言红;删/补件由 git diff 件核兜 | -| V9 反哺点清单(**非行为改动**——纯文档产出物,无游戏行为,§9.1) | T4 `reports/iter-反哺点-<完成日>.md` §6 五条各一落点(①敌人系统.md 记法措辞/②关卡节点地图.md :146/③关卡节点图生成.md 推论条款 A/④06 状态回写/⑤roadmap 编入笔) | §6 五条每条有落点〔fv26〕 件核对表(T4 出口) | 非行为改动 | 反哺点清单对 §6 同步面表件核 | 撤销证明:删一落点(如⑤)→ T4 出口件核红 | - -**旧断言预声明(test-conventions §9.5)**——允许改写的旧断言清单及理由:§8.1 S1–S16(17 行)全部条目(理由:遭遇寻址由(关卡序号×节点深度)坐标换代为(关卡序号×row_kind×深度带)带键,断言取键与定位方式随动;标注「断言语义保持」者只换键/定位、判据不动)。除此之外的断言删除/放宽/换下层直调=未声明削弱,停卡呈报、独立审计判红。 - -**独立验收审计(test-conventions §9.7,2026-10-21 起)**:合并前由未参与实现的迭代 loop 内子代理(或用户点名会话)审计——只读本卡、实现报告实际对照表、diff 与测试文件,自跑最窄测试抽查,不采信「已全绿」;审计报告落 `reports/iter-验收审计-<完成日>.md`(逐行「通过/缺漏/例外及依据」)。**审计未通过不得合并**(T4 合并前置)。 - -## 9. 异常分级 - -- **L1(自救)**:断言措辞、命名、注释、文件落点;冒烟消息文本随坐标记法更新。 -- **L2(记录继续)**:带切法实现形态(ceil 式键表扫描 vs 显式区间表——接口块语义内);校验错误类别命名细节(沿既有词表增补);冒烟断言的扫描式/resolve 式写法选择(§10 ⑤)。 -- **L3(停机呈报)**:迁移后行值与 §4.2 表对不上(数据漂移);任何触碰定位信息契约(两样)的诉求;带切法出现第二实现;任何删/增敌人内容的诉求(hobgoblin 补齐或删行);执行中发现同深度多终点图样(推论条款 A 被打破);`validate()` 旧类别出现失守缺口且无法在键换代内兜住。 - -## 10. 代拍板白名单(超清单必呈报) - -1. 带切法实现形态与带键编号规则——约束:迁移期与旧 ceil 逐位等价(契约 2)、单点收口(契约 3)、D<0 fatal 与顶 clamp+warning 去重两语义不破。 -2. schema 新字段/常量命名(`row_kind`/`depth_band`/`ROW_KINDS`)与枚举值字符串("battle"/"boss" 对齐节点 type 词表)。 -3. `resolve_enemy_rowset` 第三参命名 `row_kind`;`enc.node_type` 缺省映射 "battle"(level_runner 生产路径恒传值,缺省只服务测试直呼面)。 -4. 校验错误类别命名(沿 `dup_key`/`domain`/`rowset_rule`/`anchor` 词表;确需新类别时命名并留痕)。 -5. 冒烟断言的定位写法(表扫描式 vs resolve 式)与断言名措辞——约束:首遇定位禁写死坐标数字(契约 8)。 -6. 提交切分与 worktree 分支命名。 - -## 11. 待裁点 - -**零待裁点,开工门=U0。** - -两处灰色地带已按设计事实推导定值(用户可否决,随 T4 反哺点清单呈报可见): -- **推论条款 A(多终点深度互异,契约 6)**:备选「扩定位信息带终点标识」——淘汰依据=D2:119「定位信息只有两样」是既有定稿契约,扩它归卡②/文档批;「多终点共带共享 Boss」——淘汰依据=D3:74「多终点各配一行(走哪条路决定打谁)」明文;定值=多终点落互异深度带,表侧投影为 Boss 带键互异校验。 -- **hobgoblin 缺口行去向(契约 9)**:备选「迁移时退役隔离」——淘汰依据=用户范围句「本卡只定数据去向不补立绘」+迁移零行为变更原则(删内容属敌种内容批职权);定值=随迁留任、缺口登记随行、补齐归敌种内容批。 - ---- - -**自审五查记录(2026-10-21)**:①规格覆盖——§1 六项新增/八项完成定义逐条挂 T 卡(寻址=T1/T2、数据迁移=T1、校验=T1/T2、多 Boss=T2、首遇=T3、冒烟随动=T3、反哺点=T4),无漏项;②步骤扫描——全部步骤一步一动作一可核查结果,函数体仅 `band_of` 一处给语义、其余签名+断言;③类型一致性——接口块五签名与 T 卡引用逐字一致(`resolve_enemy_rowset(L, D, row_kind)`/`spawn` 尾参 `row_kind`/`band_of(band_keys, D)`/`ROW_KINDS`);④审查焦点——补挂三类失败模式:顶 clamp 静默消解(V4)、同带双终点(V4)、首遇行误加骰(V6),各带变异注入;⑤篇幅比例——规格≈1 屏、任务卡步骤≈实现面 1:1,无抄写体。 - -**grilling 复核记录(2026-10-21,设计树对抗复核,不采访用户——可推导定值写依据、真归用户列 §11)**:走查五支——带定义/切法(覆盖域与顶 clamp 归 §4 契约 3+契约 7,无缺口)、行类维度(battle/boss 同深度串位反证成立,row_kind 必要性已写进契约 1;node_type 非法值 fatal 已钉)、多 Boss 寻址(共带后果原文写错——「混编双 Boss」改为「两终点命中同一 Boss 行集」,契约 5 已订正;表侧/图侧投影分工已写清)、首遇定位(关卡节点地图:120「教学位同样掷骰、无结构豁免」与敌人系统「首遇不加骰」并存无冲突——前者是图侧不豁免、后者是行值无区间,契约 8 表述核对一致)、数据迁移对拍(原 V3 数值逐字段断言与 AGENTS.md:72 数值单一事实源红线冲突——改为键位/行序/计数结构断言+git diff 值列件核,变异面改钉 hobgoblin 错带)。AGENTS.md 硬红线逐条过:数值单点(本卡零数值)、测试两入口(T 卡只写断言不跑测)、实现批禁写 design/(§6 义务全走反哺点、零旧口径义务措辞)。结论:零待裁点,两处灰色推导已定值并留 §11 供用户否决。 diff --git a/reports/需求卡片-节点图消费侧换代批-2026-10-21.md b/reports/需求卡片-节点图消费侧换代批-2026-10-21.md deleted file mode 100644 index 8490f7d1..00000000 --- a/reports/需求卡片-节点图消费侧换代批-2026-10-21.md +++ /dev/null @@ -1,278 +0,0 @@ -# 需求卡片:节点图消费侧换代批(模板池消费 → 生成图消费)· 关卡节点图重构卡② - -- 版本:**v1.1**(2026-10-24 修订重锚——断言宿主随 game/smoke/ 退役〔16c6f136〕迁批 B 16 宿主、跨表校验④删除范围重划(迭代 34 检查语义承接进 S5)、run_all manifest 登记步补齐、§3 行号重锚;v1.0 2026-10-21 成文,自审五查+grilling 复核结论见文末) -- 来源:[待办清单](../待办清单.md)「关卡节点图重构——可选节点太少」(2026-10-13 登记,来源=playnote,设计面=文档轨,状态=文档设计已闭合)(唯一事实源:[关卡节点地图](../design/gdd/systems/冒险循环/关卡节点地图.md) v2.1+[关卡节点图生成](../design/gdd/systems/冒险循环/关卡节点图生成.md) v1.1+[冒险结构](../design/gdd/systems/冒险循环/冒险结构.md) 关卡行集与结构冻结行,2026-10-21 双定稿闭合凭据) -- 归属:关卡节点地图消费侧(加载校验/行进语义/商店编排冒烟/定位信息/确定性承载/旧模板清理);关卡节点图重构批卡②(卡①生成器/卡③敌人表深度带〔已落地·迭代 34〕/卡④迷雾可见性〔已落地·迭代 34〕同批) -- 排期:未编入(2026-10-18 用户令:需求卡片不预占迭代号,批次归属按工作量另裁) -- 开工门:U0(用户 `/iter` 发令)+ grilling 复核门过(零待裁点,见 §11) - -## 0. 前置依赖与并行性 - -- **设计闭合(第〇步)**:06 条目「关卡节点图重构——可选节点太少」状态=文档设计已闭合,凭据=关卡节点图生成.md v1.1+关卡节点地图.md v2.1(各带路径+版本号)→ 放行写卡。 -- **硬依赖卡①(生成器)**:运行时图来源换成生成器产图(§7 取用块),卡①接口定稿+实现落地后本卡才可合入运行时切源;T0 与卡①接口对齐(签名/图数据形状/参数档 schema/复现语义四件)。 -- **硬交付序列(跨卡数据覆盖)**:敌人深度带数据对生成图深度域的覆盖(每 (关卡序号, row_kind) 顶带键 ≥ 该关生成图最大节点深度;且多终点 Boss 各终点深度的解析带键可互异——卡③ 契约 5「同关多 Boss 行集带键互异」的图侧投影)须先落或与本卡同批落——归口=卡③ §5 已登记的「生成图深度扩面覆盖问题(卡①/后续表批)」,**尚未立卡(2026-10-24 实况登记,见待办清单 06 条目缺口)**。数据实况(迭代 34 落地后实查):敌侧战斗带顶 L1=3/L2=L3=7(L1 深度 4-5、L2 深度 8、L3 深度 8/10-12 越顶)、Boss 各关单带 4/9/9(L1 终点深度 6 越顶、L2 双终点深 8/9 共带、L3 终点深至 13 越顶);板侧 board_gen `node_depth` 精确行同域缺口——切源即 `EnemyTable.band_of`(`enemy_table.gd:157`)越顶返 -1 加载致命。未覆盖时覆盖断言(§8.1 S5)红、遭遇解析顶 clamp 消解,本卡不得单独合入(依据:冒险结构.md:293 覆盖义务+卡③契约 7 顶 clamp 禁消解+闸全绿要求)。 -- 与卡③接口零改动:定位信息契约=(关卡序号 + 节点深度) 两样(关卡节点地图.md:119;卡③ §0 同口径),本卡换传递侧值语义、不动消费侧寻址。 -- 与卡④(迷雾可见性)并行无依赖;迷雾呈现面(近处见类型/远处起雾)归卡④,本卡只保证图结构只读读面在位。 - -## 1. 目标与完成定义 - -**目标**:节点图消费侧六件换代——① 加载校验换代(多入口/多终点、每路径恰从一入口到一终点、终点恒为 Boss、无环;规则语义以关卡节点图生成 v1.1 产出规格为准,校验动作在消费侧);② 行进语义换代(大岔口自 L2 起、多 Boss 兑付与关卡收束、单链退化形态仍合法但三关参数档都含岔口);③ 商店编排冒烟断言换代(每个终点前一位恰一家店、每局必经恰一家);④ 定位信息换代(关卡序号+节点深度,弃节点编号寻址语义);⑤ 确定性承载换代(固定种子+固定参数档承载首局教学图与存档复现,「模板 0=回滚档」语义废除);⑥ 旧 15 张模板 level.tres 数据清理(生成器产图经卡①接口取用)。 - -**相对现状,新增了什么**: -1. 图来源换代:`level.tres` 模板池(3 关 × 5 张)→ 卡①生成器按(参数档,种子)产图;加载校验的对象从静态模板变为运行时生成图; -2. 校验语义换代:唯一入口/唯一 Boss/编号最大/出度 ≤2/层宽 ≤5/单链前三纯战斗六项退役,换为多入口多终点合法+路径完整性+终点恒 Boss+无环四检; -3. 行进面:多入口首跳(入口停点候选=全部入口)、任一终点 Boss 胜利即关卡收束(多 Boss 泛化)、单链退化过校验; -4. 定位信息值语义:`node_depth` 从节点编号换为节点深度(自入口步数);图内行进/断点寻址改生成图节点句柄(`_node_id` 双编号空间退役); -5. 确定性承载:`first_script`/`template_pick`/`active_template_*`/模板 0 回滚档全退役;首局教学图=种子 42+参数档生成图;断点复现=主种子+参数档重生成同图; -6. 冒烟断言面从模板形状写死断言换为生成图性质断言(商店编排三条、含岔口、确定性三条)。 - -**完成定义**(全部满足才算本批完成): -1. 加载校验四检+良构基底在位:违规生成图起关致命(无软回退、无兜底图);旧六判据零残留; -2. 多入口首跳、多 Boss 兑付收束、单链退化合法三条行进语义各有验收断言绿; -3. 商店编排冒烟性质断言(终点前恰一店/商店数=终点数/全路径恰穿一店)在三关参数档×种子扫描面全绿; -4. 定位信息=(关卡序号, 节点深度):enc 键名不变、值语义换代,与卡③带寻址衔接正常;图内寻址=句柄(读面 API 签名不变); -5. 确定性承载换代落地:同 (种子, 参数档) 两次产图逐位同图;首局种子 42 教学图恒定;断点存→恢复→再产图同图;`template_pick` 断点键退役; -6. `level.tres` 三行 `templates` 键清出(15 张模板数据清空,行级字段保留);全仓零 `first_script`/`template_pick`/`active_template` 引用(件核); -7. §8 断言批全绿+变异验证记录表进实现报告; -8. 批末重闸按改动域选档全绿+改前改后读数对拍;三份 `reports/` 产出物写齐(T4)。 - -## 2. 设计事实源 - -| # | 出处 | 取用条款 | -|---|---|---| -| D1 | `design/gdd/systems/冒险循环/关卡节点地图.md` v2.1(2026-10-21,06 闭合凭据之一) | :51 结构铁律走图三句(连线只朝前进方向无环/每条路径恰从一入口出发恰到一终点/终点恒为 Boss 可多个)+「违反任何一条的行集是配置违约,加载期拦截」;:63 商店编排(每个终点前一位恰一家店、每名玩家一局必经恰一家店、**位置约束不由加载校验承载、由冒烟断言把关**);:65 节点深度=从入口沿路走过来的步数、遭遇坐标=关卡序号+节点深度、单链合法退化、三关参数档都含岔口;:71-91 行进纪律(兑付后确认、零自动推进、入口不自动进入);:119 定位信息只有两样(关卡序号 + 节点深度);:126-136 关卡收束点(先清库后宣布);:174-186 验收标准(图合法四语义/兑付移交/商店房编排/单链特例合法) | -| D2 | `design/gdd/systems/冒险循环/关卡节点图生成.md` v1.1(2026-10-21,06 闭合凭据之一——产出规格/执行边界,本卡校验规则语义来源) | :41-51 产出约束五条(道路系统=除岔口外每节点恰一条去路、岔口去路 2–3;宽度=分叉生长;层高=沿路步数;出口归属锁定;战斗节点保底)+结构铁律(无环、入口终点可多个〔个数=参数档〕、每路径恰一入口到一终点);:53-60 执行边界(不合格产出禁止进玩法、无兜底图、参数无解即致命中止、同种子+同参数档产出同一张图、随机只经 RngService 统一流);:62-72 五旋钮与三套参数档(生成器只吃参数档不认关卡号;第一关即含岔口;单链是分支密度取下限的合法退化形态、不作为三关参数档取值);:78-84 摆放约束(商店位=每个终点前一位恰一家商店);:101-115 调参表(层深 L1 ≥6 已锁、出口个数 L1 恒 1/L2/L3 1–3 已锁、商店数=出口数已锁、其余 provisional);:117-128 验收标准 | -| D3 | `design/gdd/systems/冒险循环/冒险结构.md`(状态:已定稿) | :264 关卡行集行(关卡表逐关行承载关数与图的参数档;「旧模板池随实现批清数据」——本卡即该清理义务;「确定性 = 固定种子+固定参数档(首局教学与存档复现的固定图由此承载,无兜底模板)」);:275 结构冻结(终点恒为 Boss、终点可多个各在路径最深处;收束点先清库后宣布的次序;一冒险一主种子);:293 覆盖义务(行集内每个战斗与关底节点(关,深)在敌人与棋盘生成的数值表中有行——措辞随深度带换代见 §6 反哺点②) | - -闭合凭据一致性:D1/D2 与 06 条目状态内列的两份定稿逐字一致;D3 关卡行集行与结构冻结行同向,且明文把「旧模板池清理」预写为实现批义务。 - -## 3. 现状事实(2026-10-21 成文实查;行号与断言宿主 2026-10-24 v1.1 重锚——对齐 game/smoke/ 退役〔16c6f136,1065 条断言经批 B 迁入 16 宿主〕与迭代 34 落地后 master 现态;开工日 T0 复核行号) - -| # | 事实 | 出处 | -|---|---|---| -| F1 | 图合法性校验唯一落点=`LevelTable.validate()`(表加载期、ConfigDB 收敛 fail-fast):单链校验体 `_validate_level_nodes`(Boss 计数恰一 :83-84、战斗 ≥2 :85-86、单链末 Boss :87-90、单链前三纯战斗 U2 规则 :94-98);图合法性核心 `_validate_graph`(悬空出边+前进方向「目标编号严格递增 ⇒ 无环」:126-132、**出度上限 2** :133-138、**入口唯一〔无入边恰一〕** :139-149、**Boss 唯一终点+无出边+编号最大** :150-157、双向可达〔无孤儿+全员达 Boss〕:159-182、BFS 层宽 ≤5 U2 规则 :183-201);id 解析双编号空间(图=node_depth/单链=数组下标):104-125 | `game/data/schemas/level_table.gd:21-58`/:61-99/:104-201 | -| F2 | 模板池加载路径:`level.tres` 三关各 5 张模板(15 张;L1 五张=单链排列变体、L2/L3 网状 10 节点);`LevelTable.validate` 池分支(池内全模板加载期校验);`LevelRunner._resolve_template_row`(template_pick ≥0 强制取模板/−1 经 `rng.open_stream("node_map")` 抽签;`active_template_name` 供 MC 分层出数、`active_template_index` 供断点备案);`start_level`/`start_level_external` 各带 `template_pick` 尾参;池解析在播种后(EC7) | 模板数据 `game/data/tables/level.tres:8-10`+表尾注释(t5/U2 生成史);校验分支 `level_table.gd:40-57`;解析 `game/core/level_runner.gd:300-324`(:304-305/:307/:317-318);尾参 :130/:186;播种序 :162 | -| F3 | 首局/回滚档(模板 0)读取处:`main.gd` `first_script = not save_manager.is_first_adventure_done()` 传入冒险参数面;种子三分判定(①显式合法整数②**首局固定 42**〔教学重放模板 0 即在此种子下标定〕③非首局 randi())单点=`_seed_value()`;`AdventureRunner._first_script`(true=全关零抽签取池内模板 0)→ `template_pick = 0 if _first_script else -1`;`SaveManager.is_first_adventure_done/mark_first_adventure_done`(存档布尔位,置位只在冒险收束挂点);断言段「模板 0 = 现行固定图(回滚档成员)」(池=5/强制 0 逐位相等/N=1 回滚档形态/first_script 分支/收束置位)——**宿主=批 B 裁决六整套迁集成** | `game/presentation/main.gd:594-596`/:1476-1490(_seed_value 三分判定);`game/core/adventure_runner.gd:45`/:102/:194/:850;`game/core/save_manager.gd:301-311`;`game/tests/integration/node_map_templates_test.gd:32-104`(:44 池=5/:45 模板 0 现行/:52-59 强制零抽签逐位相等/:76 N=1/:82 首次脚本/:96-101 布尔位与收束置位;manifest 已在册) | -| F4 | 商店「Boss 前一位必经」断言:`_smoke_shop_graph` 硬编码旧模板形状(商店房恰 1 且 node_depth=8〔next=[9]〕、Boss=9=全图最大编号、DFS 枚举全路径穿商店房 8、跨表 (·,9)/(·,8) 行覆盖+「L1 行集零改动(单链 Boss (1,4))」);路径枚举 `_all_entry_boss_paths`(DFS 迭代栈);行进断言「L2 行进到商店房(节点 8,next=[9]——Boss 前必经位)」;模板感知取节点表 `_level_active_nodes`(恒取模板 0)——**宿主=批 B 迁 G8 单元宿主** | `game/tests/unit/core/level_shop_economy_test.gd:679-715`(_smoke_shop_graph::696 商店恰 1 depth=8/:697 Boss=9 最大编号/:698-703 DFS 穿店/:704-714 跨表行覆盖与 L1 行集零改动)/:936(_all_entry_boss_paths)/:601(行进到商店房)/:970(_level_active_nodes) | -| F5 | Boss 编号最大语义:`_validate_graph` ⑤「Boss 恒唯一终点:无出边 + 编号最大」——`ids[i] != ids.max()` 报「终点编号最大」;连带 :83-84 Boss 计数恰一 | `game/data/schemas/level_table.gd:150-157`/:83-84 | -| F6 | 定位信息与寻址现状:`begin_battle` 组装 enc=`level_index` + `node_depth`(**值实为节点编号**)+ `node_type`;`_node_id` 双编号空间(图形态=node_depth 当节点身份/单链=数组下标,混用注记在案);`_entry_node` 取无入边**唯一**节点;`pick_next_node(node_id)` 按编号选路;断点 `node_id`/`visited_ids` 同编号空间;战斗侧按 (level_index, node_depth, row_kind) 三参解析敌行集(`resolve_enemy_rowset`——迭代 34 卡③ 落地形态) | 组装 `game/core/level_runner.gd:513-525`(:514/:524);`_node_id` :341-347;`_entry_node` :348-358;`pick_next_node` :619 起;断点 `game/core/adventure_runner.gd:1000`/:1017;消费 `game/core/battle_orchestrator.gd:329`(resolve_enemy_rowset 三参)/:357/:447/:1054;跨表覆盖校验 `game/core/config_db_cross_validator.gd:54-106`(模板池感知分支 :76-83;敌侧带寻址+顶 clamp 拦截 :90-102;**迭代 34 写入「同图多终点 Boss 解析带键互异」:103-106——T2 删除范围须按 v1.1 重划**) | -| F7 | 行进语义现状:defer_entry 入口停点(单候选=唯一入口)、`present_entry_choice`、兑付确认后 `_present_choices` 出边候选、零自动推进;`_on_battle_finished` 以「节点 type=="boss"」判关卡收束(Boss 胜=victory、任意败=defeat,多 Boss 场景本就泛化);单链隐式出边(无 next 键=数组序下一项)+`_graph_mode` 探测 | `level_runner.gd:184-233`(:219-223/:228-233)/:561-569/:556-562(:562 boss 判收束)/:360-385(单链隐式出边在 _out_ids/_out_nodes 体内)/:149-158、:202-205 | -| F8 | 断点与确定性承载现状:快照 28 键含 `template_pick`(=`active_template_index`)、`master_seed`、`rng_counter`、`node_id`、`visited_ids`;续玩 `resume_adventure` 回填 `rng.restore_adventure(master_seed, rng_counter)`+强制 `template_pick`(已抽模板续玩零重抽零 RNG 消耗);rng 快照契约=主种子+计数器 | `game/core/adventure_runner.gd:998-1023`(:1000/:1013/:1014/:1015/:1017)/:140-148/:852-854;`game/core/rng_service.gd:4`/:77-83(restore_adventure) | -| F9 | 模板/first_script 依赖面(game/smoke/ 退役后现役清单,2026-10-24 实查,共 12 文件):产品面 4 件——`adventure_runner.gd` 7 处(:45/:102/:194/:850/:852-854/:863/:1013)、`level_runner.gd` 17 处(尾参 :130/:186、解析体 :300-321、active_template_* :304-305)、`main.gd` 2 处(:594-596)、`game/sim/level_autoplay.gd` 7 处(尾参 :37/:63、`level_templates` :141/:200-202、route 轴注释「候选节点编号最小/最大」:12/:377-379);测量面 1 件——`game/sim/adventure_monte_carlo.gd`(`level_templates` 出数面 :139);测试面 7 件——`game/tests/integration/node_map_templates_test.gd`(S1 全段 :32-104)、`game/tests/integration/cross_system_flow_test.gd:79`(`{"first_script": true}` 驱动)、`game/tests/unit/core/meta_progression_forge_test.gd:316/:323-324`(参数面 extra 键样例)、`feat_vouchers_test.gd:25`、`pool_case_d_test.gd:37/:68/:113/:142/:160`(均传 `{"first_script": true}`)、`adventure_feats_test.gd`(模板感知取节点表 :812-816+en_keys 覆盖断言 :112-130)、`level_shop_economy_test.gd`(`_level_active_nodes` :970 及消费 3 处) | 各文件如列 | -| F10 | 形状写死的断言面(模板时代遗留,**宿主=批 B 迁入**):「L2/L3 节点数 = 10」`level_shop_economy_test.gd:102/:257`、「L1 行集零改动(单链 Boss (1,4))」:714、逐节点扫描断言 :105-110/:211-219/:260-274、参数面断言「入口特有键(first_script/shop_mode)随 extra 原样并入」`meta_progression_forge_test.gd:316/:323-324`;历史生成工具 `tools/t5_build_templates.py`/`t17_fanout2_rebuild.py`(一次性内容批工具留档,在盘) | `game/tests/unit/core/level_shop_economy_test.gd` 与 `meta_progression_forge_test.gd` 如列;`tools/t5_build_templates.py:8` 等 | - -## 4. 设计契约(红线,违反 = L3 停机呈报) - -🔴 **契约 1 · 加载校验换代与 fail-fast**:加载校验(生成图到手、进节点前执行)=四检+良构基底——① 多入口/多终点合法(入口〔无入边节点〕≥1、终点〔无出边节点〕≥1;入口/终点个数与参数档的一致性归卡①产出侧——D1:176 加载校验清单无此项,本卡不核个数);② 每条路径恰好从一个入口出发、恰好到达一个终点(全部节点从某入口可达+可达某终点,无孤儿无断头);③ 终点恒为 Boss(每个终点 type="boss" 且每个 boss 节点无出边);④ 无环(出边目标节点深度严格大于源——连线只朝前进方向);良构基底=节点句柄唯一、出边引用全部命中、type ∈ 五类枚举、battle/boss 节点带节点深度。违规=致命(push_error+起关拒绝,冒险启动失败走既有 fatal 路径),无软回退、无兜底图(D2 执行边界「无效图绝不上给玩家」)。 - -🔴 **契约 2 · 弃节点编号寻址**:定位信息=(关卡序号, 节点深度)——`node_depth` 键名保留、**值语义换代为自入口步数**(现值实为节点编号,F6);图内行进/断点/呈现读面的节点身份=生成图节点句柄(卡①产出的唯一 id),禁再以 `node_depth` 兼任节点身份(`_node_id` 双编号空间退役)。定位信息两样契约不破(D1:119;卡③ §0 同口径)。 - -🔴 **契约 3 · 确定性承载=固定种子+固定参数档**:图=(种子, 参数档) 的纯函数——同 (种子, 参数档) 两次产图逐位同图,复现不依赖统一流全局计数器位次(D3:264「首局教学与存档复现的固定图由此承载」+D2 执行边界「同种子+同参数档产出同一张图」合取);首局教学图=首局固定种子 42(main.gd 种子三分判定保持)+固定参数档生成图;存档复现=断点 `master_seed`+参数档重生成同图(断点零新增图身份键);「模板 0=回滚档」语义废除——模板池、`template_pick` 强制、`active_template_*`、`first_script` 参数键全退役(F3/F9 清单)。**旧断点代差显式处置**:断点快照扩列代差标记键(缺省 0/写 1,扩列不升版先例);续玩遇缺标记键的旧断点=`resume_adventure` 显式拒绝(返回 false+push_error,禁按句柄语义静默回放——旧 `node_id` 值与新句柄空间可碰撞错位),旧模板图已随模板池退役、无迁移动作可做(禁凭空捏图=无兜底图红线);处置留痕实现报告。 - -🔴 **契约 4 · 商店编排语义不破**:每个终点前一位恰一家店(终点直接前驱恰为一家 shop 节点)、商店总数=终点数、所有入口→终点路径穿过且恰穿过一家店(D2 调参表「商店数=出口数」已锁+D1:63)。位置约束**不由加载校验承载、由冒烟断言把关**(D1:63 明文),加载校验禁新增商店位检查。 - -🔴 **契约 5 · 行进纪律与收束次序不破**:兑付后确认+零自动推进不变;入口不自动进入(多入口时首跳=「从哪进场」的选择,入口停点候选=全部入口节点);任一终点 Boss 战胜利即关卡收束(多 Boss 泛化,走哪条路兑付哪个 Boss)、英雄死亡=失败;收束次序先调战斗结算清库、后宣布关卡结束(D3:275 结构冻结);单链退化形态合法(校验不设「必有岔口」),三关参数档都含岔口是参数/生成面事实(冒烟挂验)。 - -🔴 **契约 6 · 模板清理义务**:15 张模板数据与模板池代码路径(含冒烟/单测引用面,F9/F10)全部清退,禁留死代码死断言;参数档数据(五旋钮 schema 与初值)不归本卡——随卡①参数 schema 落位,消费侧只读不值(D2「生成器只吃参数档」+数值事实源=data/tables)。 - -🔴 **契约 7 · 接口纪律**:生成器产图取用卡①接口(§7 取用块;签名与图数据形状以卡①定稿为准,**两条硬语义不随签名浮动**:同 (种子, 参数档) ⇒ 同图;纯函数复现);与卡③的定位信息传递契约零改动(enc 键名 `level_index`/`node_depth`/`node_type` 保持);本卡不实现生成算法、不动敌人表深度带数据(卡③)、不做迷雾可见性(卡④)、不落参数档数值(定值批/难度曲线)。 - -🔴 **契约 8 · 多终点深度互异(跨卡推论条款 A,卡③契约 6 同源)**:定位信息只有两样且「多终点各配一行(走哪条路决定打谁)」⇒ 多终点必须落在互异深度(同深度多终点=设计不可能态);图侧兑现归卡①产出规格(卡③ §6 反哺点③已布),本卡不新增校验;执行中发现同深度多终点图样 → L3 呈报。 - -## 5. 范围边界 - -**做**:加载校验四检+良构(`level_table.gd` 换代);行进语义三件(多入口首跳/多 Boss 收束/单链退化合法);定位信息值语义换代+句柄寻址;确定性承载换代(first_script/template_pick 链路退役、断点键换代);旧 15 张模板 `level.tres` 数据清理+模板池代码路径清退;冒烟断言面换代(商店三条性质断言、含岔口、确定性三条);断言批与变异验证;反哺点清单。 - -**不做**(明示去向): -- 生成器算法本体(图怎么长、多终点怎么切、产出约束五条的产出侧自证与测试)→ **卡①**;本卡只取用其产图接口; -- 敌人表深度带数据、带切法、`resolve_enemy_rowset` 三参化 → **卡③**(已写卡);本卡只保证传递侧 (关卡序号, 节点深度) 值语义正确; -- 迷雾可见性(近处见类型/远处起雾、Boss 标记不入雾)→ **卡④**; -- 生成图深度扩面的敌/板表数据覆盖(深度带补行/带切法粗化、board_gen/difficulty 键带化)→ 卡③ §5 已登记归口(卡①/后续表批);本卡只把交付序列写死(§0),不扩数据; -- 参数档数值(层深/分支密度/配比/入口个数/出口个数的取值)→ 定值批落表(D2 调参表状态列),本卡零数值; -- 呈现层改版(level-map 屏面布局、节点编号显示、多终点标记画法)→ level-map/HUD 框架与卡④;本卡只保证 runner 只读读面签名不变; -- 历史生成工具 `tools/t5_build_templates.py`/`t17_fanout2_rebuild.py` → 一次性内容批留档不动(`level.tres` 表尾注释已载明)。 - -## 6. 关联文档同步面 - -> 本批是代码实现批,禁写 `design/` 整目录与根规则文件,唯一可写目录=`reports/`。下表「同步义务」一律执行为:逐条列进 `reports/iter-反哺点-<完成日>.md`(哪个文件哪一节、改成什么、依据),用户会话裁决后归文档批落文。 - -| 文档 | 关联点(本批实现会改变它的哪条陈述) | 同步义务 | -|---|---|---| -| `design/gdd/systems/冒险循环/关卡节点地图.md` | :57「战斗|携带节点编号」、:156「组装关卡序号 + 节点编号」、:178「每场开战调用携带(关卡序号 + 节点编号)」三处与 :23/:65/:111/:119「关卡序号 + 节点深度」不一致 → 统一为「关卡序号+节点深度(自入口步数)」(本卡契约 2 弃节点编号寻址的文档面) | 反哺点 · 列清单(①用词统一,依据=D1:65/:119 定稿句+用户卡②范围句) | -| `design/gdd/systems/冒险循环/冒险结构.md` | :293 覆盖义务「每个战斗与关底节点(关,深)在敌人与棋盘生成的数值表中有精确行」→ 深度带换代后措辞改「敌侧深度带命中+板侧解析命中」;:264 关卡行集行「旧模板池随实现批清数据」括注在本卡完成后成既成事实(括注注销) | 反哺点 · 列清单(②措辞换代+括注注销,依据=卡③契约 1/本卡完成定义 6) | -| `design/gdd/systems/冒险循环/关卡节点图生成.md` | 无改动(本卡消费其产出规格与执行边界)——但校验四检的实现措辞如与产出规格用语需对齐(「岔口」「终点」「路径」定义引用),T1 注释引用其节号 | 反哺点 · 列清单(③仅在实现注释发现用语漂移时列,无漂移则记「无」) | -| `design/gdd/ux/level-map.md` | 图结构(多入口/多终点/含岔口)呈现面随动义务(06 条目连带面②)——本卡不改呈现代码,读面 API 签名不变(句柄化后编号显示语义漂移:显示的是句柄) | 反哺点 · 列清单(④呈现面随动需求登记归卡④/level-map 文档批) | -| `待办清单.md`(06 登记处) | 本卡登记条状态推进「已写卡(`reports/需求卡片-节点图消费侧换代批-2026-10-21.md`)」(req-card 06 状态回写义务);实现批完成后按退役规则处理 | 反哺点 · 列清单(⑤状态回写与条目退役记载——本会话文件面限于本卡一个文件,回写由批次收口/用户发令执行,防四卡并行互踩) | -| `roadmap.md` | 迭代行编入笔与批内状态刷新 | 反哺点 · 列清单(⑥) | -| `game/data/schemas/level_table.gd`、`game/data/tables/level.tres`、`game/core/level_runner.gd`、`game/core/adventure_runner.gd`、`game/core/config_db_cross_validator.gd`、`game/presentation/main.gd`、`game/core/save_manager.gd` | 消费侧换代与模板清理本体 | 实施本体(可写) | -| `game/tests/integration/node_map_templates_test.gd`、`game/tests/unit/core/level_shop_economy_test.gd`、`game/tests/unit/core/meta_progression_forge_test.gd`、`game/tests/unit/core/adventure_feats_test.gd`、`game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd`、`game/tests/unit/core/config_db_cross_validator_test.gd`、`game/tests/unit/core/breakpoint_resume_test.gd`、`game/tests/unit/core/pool_case_d_test.gd`、`game/tests/unit/core/feat_vouchers_test.gd`、`game/tests/run_all.gd` | 断言面换代与 manifest 登记(§8 S 清单——game/smoke/ 已退役,批 B 后断言面全落 16 宿主) | 实施本体(可写) | -| `game/sim/level_autoplay.gd`、`game/sim/adventure_monte_carlo.gd`、`game/probe/shot_probe.gd`、`game/probe/ui_click_probe.gd` | 驱动/测量/探针面换代(S7) | 实施本体(可写) | - -## 7. 任务卡 - -> 估时按 Claude 工作日口径;每卡出口自跑轻闸;开工日 T0 复核本卡全部行号。回归措辞模板沿用:里程碑步(T 段)出口跑一次全套闸;波内修复只做出红最小单元的最窄确认;新增测试先行孤立验证再并入全套。 - -### 卡前 · 文件结构图 - -| 文件 | 职责 | -|---|---| -| `game/data/schemas/level_table.gd` | 图合法性校验换代:四检+良构基底(`validate_graph` 静态入口);旧六判据与模板池校验分支退役;行级校验保留 | -| `game/data/tables/level.tres` | 15 张模板数据清理(三行 `templates` 键删除;行级字段保留;参数档字段随卡① schema 不归本卡) | -| `game/core/level_runner.gd` | 图来源换代(卡①产图替代 `_resolve_template_row`)+起关校验调用+多入口首跳+句柄寻址(`_node_id` 双空间退役)+enc `node_depth` 值语义换代 | -| `game/core/adventure_runner.gd` | `first_script`/`template_pick` 链路退役;断点键换代(`template_pick` 退役、`node_id`/`visited_ids` 句柄化) | -| `game/core/config_db_cross_validator.gd` | ④ 关卡节点半边整体退役(:54-106,随静态节点数据消亡——含迭代 34 多终点 Boss 带互异检查的静态形态,语义由 S5/V11 运行时断言承接;跨表校验单测 `_test_level_node_band_rules` 段同步退役) | -| `game/presentation/main.gd` | `first_script` 参数构造退役(种子三分判定保持不动) | -| `game/core/save_manager.gd` | `is_first_adventure_done` 注释语义换代(模板 0 教学重放 → 首局固定种子;键与行为不变) | -| `game/tests/integration/node_map_templates_test.gd`(改)、`game/tests/unit/core/level_shop_economy_test.gd`(改)、`game/tests/unit/core/meta_progression_forge_test.gd`(改)、`game/tests/unit/core/adventure_feats_test.gd`(改)、`game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd`(改)、`game/tests/unit/core/config_db_cross_validator_test.gd`(改)、`game/tests/run_all.gd`(改) | 断言面换代(S1–S8):模板段退役+商店断言换代+形状断言性质化+生成图取数面+④ 静态检查退役随动+新套件 manifest 登记 | -| `game/tests/unit/core/level_table_validate_test.gd`(新建) | 四检+良构单测(正向/负向夹具图) | -| `game/tests/unit/core/breakpoint_resume_test.gd`、`game/tests/unit/core/pool_case_d_test.gd`、`game/tests/unit/core/feat_vouchers_test.gd` | 断点键换代随动、`first_script` 键清退 | - -### 接口块(全卡共用,后卡引用以此为准) - -- **取用卡①产出:生成器产图接口**(期望签名,**以卡①定稿签名为准**):`generate(level_index: int, params: Dictionary, seed: int) -> Dictionary`。返回期望形状:`{"nodes": Array[Dictionary], "entries": Array[int], "terminals": Array[int]}`;节点期望字段:`{"id": int(图内唯一句柄), "type": String(battle/boss/reward/rest/shop), "depth": int(自入口步数), "next": Array[int](出边目标句柄)}`。**两条硬语义不随签名浮动**:① 同 (seed, params) ⇒ 逐位同图(D2 执行边界);② 纯函数复现——图=(seed, params) 的函数,复现不依赖统一流全局计数器位次(D3:264 存档复现承载合取)。生成器内部取数仍经 RngService(禁自建 RNG 红线不破)——纯函数复现与统一流的机制协调(derive_at 纯派生/号位内重放/RngService 纯流面)归卡①接口定稿,硬语义不变。图数据内部适配(若卡①定稿字段名为 `node_depth`/`depth` 或 id 载体不同)在 T0 对齐后按定稿接线,enc 键名不变。 -- **取用卡①产出:参数档 schema**(期望五旋钮行字段:层深/分支密度/配比/入口个数/出口个数,落 `level.tres` 关卡行;字段名与初值以卡①定稿为准,数值归定值批)——消费侧只读该参数档传给 `generate`,不自定值。 -- **产出(供战斗侧/卡③)**:`begin_battle` enc 保持 `level_index: int` + `node_depth: int`(**值=节点深度**)+ `node_type: String`;与卡③ `resolve_enemy_rowset(L, D, row_kind)` 衔接零改动。 -- **产出(供呈现层)**:runner 只读读面签名不变——`nodes() -> Array`、`node_id_of(node) -> int`(句柄)、`node_out_ids(node) -> Array`、`current_node_id() -> int`、`next_candidate_ids() -> Array`、`visited_ids() -> Array`、`pick_next_node(node_id: int) -> bool`。 -- **校验入口**:`LevelTable.validate_graph(nodes: Array) -> Array`(静态函数,返回错误字典列表——与现役 `validate()` 错误形态同构;落点 `level_table.gd`,校验体单一住所不拆)。 - -### T0 事实坐实与卡①接口对齐(≈0.5–1d) - -- [ ] 复核 §3 全部 file:line 与 §2 取用条款行号,漂移即订正本卡——可核查结果:订正记录或「零漂移」声明进实现报告素材。 -- [ ] 与卡①接口对齐四件:产图签名、图数据形状(id/type/depth/next 字段名)、参数档 schema 字段名、复现语义(纯函数复现两条硬语义确认)——可核查结果:对齐纪要(定稿签名对照本卡接口块,漂移处回写本卡接口块)。 -- [ ] 生成图深度域实测:对 L1/L2/L3 参数档各以种子 42 生成,记录最大节点深度;对照敌/板表覆盖(敌侧顶带键、板侧键域)——可核查结果:覆盖实测表在案;缺覆盖(预期如此,§0 硬交付序列)则登记「数据覆盖批先行」开工前置,缺覆盖状态下不进 T2 切源合入。 -- [ ] 改前读数基线采集(冒烟/单测通过数,两入口铁规下随 T1 迭代 loop 首跑取值)——可核查结果:基线读数记录在案。 -- [ ] §6 反哺点①-⑥ 条目起草(一节一改一依据)——可核查结果:反哺点草稿段落在 T4 产物成文前就绪。 - -### T1 加载校验换代(≈1.5–2d) - -- [ ] 实装 `LevelTable.validate_graph(nodes: Array) -> Array`(签名与判据=契约 1 四检+良构基底;落点 `level_table.gd`)。可核查结果:正向夹具图(多入口多终点/含岔口/单链)零错误;负向夹具图(非 Boss 终点/带出边的 Boss/环/孤儿节点/断头节点/悬空出边/句柄重复/缺 depth 的战斗节点)各报对应错误类别(沿 `structural`/`dangling_next`/`dup_key`/`enum`/`domain` 词表)。 -- [ ] 退役旧判据六项(`level_table.gd:83-84` Boss 恰一、:85-86 战斗 ≥2、:87-90 单链末 Boss、:94-98 前三纯战斗、:133-138 出度 ≤2、:155-157 编号最大)与模板池校验分支(:40-57),行级校验(level_index 唯一/display_name/rest_heal_pct 域)保留。可核查结果:`validate()` 对生产表行级零错误;全仓检索六判据消息文本零残留。 -- [ ] 实装无环判据=出边目标节点深度严格大于源(取代「目标编号严格递增」,:126-132 换代)。可核查结果:回边夹具(目标深度 ≤ 源)报「违反前进方向」错误。 -- [ ] manifest 登记:`game/tests/run_all.gd` `_suites()` 追加 `{"path": "res://tests/unit/core/level_table_validate_test.gd", "expect": 0}`(对齐既有行体例;批 B 协议——禁「文件在 manifest 不在」空窗)。可核查结果:run_all 实跑套件数 +1 且新套件 PASS。 -- [ ] 验收步骤:跑 `godot --headless --path game --script res://tests/unit/core/level_table_validate_test.gd`,期望四检正负向断言全绿。 -- [ ] 变异验证(注入→最窄闸见红→`git checkout` 还原→记录):注释掉「终点恒为 Boss」判据 → 期望非 Boss 终点负向用例红;把无环判据改成恒真 → 期望回边负向用例红。记录表进实现报告。 - -### T2 消费侧切源与行进/定位/确定性换代(≈2–2.5d) - -- [ ] 实装图来源换代:`level_runner.gd` `_resolve_template_row`(:300-324,含 :300-305 解析注释与 `active_template_name`/`active_template_index` 声明)删除,起关路径改调卡① `generate(level_index, params, seed)`(params 读关卡行参数档、seed 取本局种子口径——单关自播种/冒险主种子派生,以卡①定稿为准)。可核查结果:起关拿到生成图(夹具参数产图可走通进节点)。 -- [ ] 实装起关校验接线:生成图到手、进节点前调用 `LevelTable.validate_graph`,违规 push_error+返回 false(起关拒绝,冒险启动失败走既有 fatal 路径——`main.gd:396` `_fatal_screen`,启动失败挂点 :597)。可核查结果:违规夹具图起关被拒(返回 false)。 -- [ ] 实装多入口首跳:`_entry_node`(:348-358)换 `_entry_nodes` 返回全部无入边节点;`defer_entry`/`present_entry_choice` 入口停点候选=全部入口(单入口行为逐位不变)。可核查结果:双入口夹具图首跳候选数=2,pick 任一入口正确进入。 -- [ ] 实装句柄寻址:`_node_id`(:341-347)改返回节点句柄(卡① id 字段);`_out_ids`/`_out_nodes`/`visited_ids`/`pick_next_node`/`_restore_to_stop` 同空间随动;单链隐式出边(:360-385 数组序,_out_ids/_out_nodes 体内)退役(生成图恒带显式出边,单链=出度 ≤1 的图)。可核查结果:单链夹具图(显式出边)行进走通;`pick_next_node` 候选外句柄拒绝。 -- [ ] 实装定位信息值语义换代:`begin_battle` enc(:513-525)`node_depth` 取节点 `depth` 值(自入口步数);`level_finished` 载荷位置字段句柄化;enc 键名不变。可核查结果:载荷断言(level_index/depth 与图上节点一致)绿。 -- [ ] 实装 `adventure_runner.gd` 模板链路删除:`_first_script` 字段与参数读入(:45/:102/:194)、`template_pick` 计算与传递(:850/:852-854/:863)整体删除。可核查结果:全仓检索 `_first_script`/`template_pick` 零引用(`level_autoplay`/单测面随动见 T3)。 -- [ ] 实装断点键换代:`_snapshot_breakpoint` 的 `template_pick` 键(:1013)删除、扩列代差标记键(契约 3);`node_id`/`visited_ids`(:1000/:1017)句柄化落档与 `_restore_to_stop` 回读同空间;`resume_adventure` 遇缺标记键旧断点显式拒绝(返回 false+push_error)。可核查结果:断点写读往返无 `template_pick` 键、句柄往返一致;旧形态夹具断点被拒(`breakpoint_resume_test` 面)。 -- [ ] 实装 `main.gd` `first_script` 参数构造删除(:594-596);种子三分判定(:1476-1490,`_seed_value` 单点)零改动。可核查结果:首局(`is_first_adventure_done==false`)种子=42、非首局随机两态行为不变。 -- [ ] 实装 `save_manager.gd:301-305` 注释语义换代(「模板 0 教学重放」→「首局固定种子」;键与行为零改动)。可核查结果:注释与契约 3 同口径。 -- [ ] 实装跨表校验 ④ 关卡节点半边退役+迭代 34 检查语义承接:`config_db_cross_validator.gd:54-106` ④ 整段(模板池感知 :76-83、node_lists 循环、敌侧带寻址/顶 clamp 拦截 :90-102、**迭代 34 写入的「同图多终点 Boss 解析带键互异」:103-106**)随静态节点数据消亡整体删除——该检查的对象=表内静态节点,切源后 `level.tres` 行不再携带节点数据,留置即死代码(契约 6 禁留);其两条拦截语义由 S5 运行时断言承接(顶 clamp 禁消解=`band_of` 非 -1;多终点 Boss 带键互异——对真实生成图逐终点解析比对,拦截面从静态代理升级到消费图本体);`config_db_cross_validator_test.gd` 的 `_test_level_node_band_rules` 段(:19 调用行)同步退役。可核查结果:全仓检索 `node_lists`/「模板池感知」/`level_node_dangling` 零残留;S5 承接断言在位(T3)。 -- [ ] 验收步骤:T 段出口跑一次全套闸(`tools/run_all_gates.sh` 按改动域选档),期望除 §8 S 清单未换代条目外零红(S 清单随 T3 收口)。 -- [ ] 变异验证:把多入口候选写死单入口 → 期望多入口首跳断言红;把 enc `node_depth` 值取句柄 → 期望定位信息载荷断言红;把断点 `template_pick` 键偷偷写回 → 期望断点键退役断言红。记录表进实现报告。 - -### T3 模板清理与冒烟断言换代(≈2–2.5d) - -- [ ] 实装 `level.tres` 三行 `templates` 键删除(15 张模板数据清出;`level_index`/`display_name`/`rest_heal_pct` 保留)。可核查结果:`git diff` 仅删模板数据段+表尾注释一段(15 张模板去向+参数档字段指引——沿 `level.tres` 既有表尾注释体例);冒烟表校验段行级零错误。 -- [ ] 换代模板断言段(S1):`_smoke_node_map_templates`(`game/tests/integration/node_map_templates_test.gd:32-104`)①-⑥ 退役(node_map 标签/池=5/模板 0=现行/强制 0/N=1 回滚档/first_script 分支),⑦ 收束置位断言保留改写(语义换「首局种子 42」注释);替代断言=确定性三条(S6,同宿主落位)。可核查结果:该宿主绿且消息文本无「模板 0/回滚档」字样。 -- [ ] 换代生成图取数面(S2):`_level_active_nodes` 三宿主副本(`level_shop_economy_test.gd:970`、`adventure_feats_test.gd:812-816`、`round_flow_skill_pool_test.gd:357`)改经卡①接口取生成图(固定种子+关卡行参数档);消费点(level_shop :102/:105/:211-219/:260-274/:689/:698、adventure_feats :118-130、round_flow :319/:330)随动。可核查结果:三宿主绿、取数面零模板引用。 -- [ ] 换代商店断言(S3,三条性质断言):① 每个终点的直接前驱恰为一家 shop 节点(终点入边恰一且 type=="shop");② 商店总数=终点数;③ 所有入口→终点路径穿过且恰穿过一家店(DFS 枚举换代 `_all_entry_boss_paths`(:936)消费句柄/深度,判定=路径上 shop 计数恰 1);断言面=L1/L2/L3 参数档 × 种子扫描(42 与 range(42, 62) 全种子);硬编码「node_depth 8/9/穿节点 8/L1 行集零改动」退役(:696/:697/:698-703/:714);:601 行进到商店房断言改性质(路径上恰一店)。可核查结果:三性质断言在扫描面全绿。 -- [ ] 换代形状写死断言(S4)::102/:257「节点数 = 10」改生成图读数性质(节点数 ≥ 层高下限+随图);:714「L1 行集零改动(单链 Boss (1,4))」退役(单链模板消亡,单链合法性改由 V2 夹具承载);逐节点扫描断言(:105-110/:211-219/:260-274)敌侧键面随卡③ S 清单、本卡只换取数面。可核查结果:清单条目逐条勾销、断言面全绿。 -- [ ] 换代覆盖义务断言(S5,承接跨表校验④退役后的两条语义):节点源换生成图后,敌侧覆盖判据=`EnemyTable.band_of`(`game/data/schemas/enemy_table.gd:157`)非 -1(禁顶 clamp——承 `config_db_cross_validator.gd:98-102` 静态拦截语义)+**多终点 Boss 解析带键互异**(承迭代 34 `:103-106` 检查语义——对生成图逐终点 depth→`band_of` 解析比对,共带即红);板侧=board_gen 解析命中非空;以 §0 硬交付序列(敌侧带数据覆盖+Boss 带键可互异)为绿灯前提。可核查结果:覆盖与带互异断言在扫描面全绿(数据批先行后)。 -- [ ] 新增确定性断言(S6,落点 `game/tests/integration/node_map_templates_test.gd`——S1 同宿主,模板段退役后该宿主承载生成图确定性断言;manifest 已在册零新增登记):① 同 (种子, 参数档) 两次 `generate` 逐位同图;② 首局种子 42 两次开局(`is_first_adventure_done==false`)生成图逐位同图;③ 断点写→`restore_adventure`→再产图同图;④ `level.tres` 无 `templates` 键。可核查结果:四断言绿。 -- [ ] 换代驱动/工具面(S7):`game/sim/level_autoplay.gd` `template_pick` 尾参退役(:37/:63)、MC 字段 `level_templates`(:141/:200-202)改生成图身份字段(`level_maps`,值=种子+参数档标识)、route min/max 轴(:12/:377-379)改句柄升/降序(确定性轴保持)、`game/sim/adventure_monte_carlo.gd:139` 出数面随动;探针注释与驱动假设(`game/probe/shot_probe.gd:12-13`、`game/probe/ui_click_probe.gd:373`「L2 商店为 Boss 前必经位」)性质化。可核查结果:autoplay/探针跑通、MC 出数字段名更新。 -- [ ] 换代单测随动(S8):`breakpoint_resume_test.gd`(断点无 `template_pick` 键、`node_id`/`visited_ids` 句柄往返、复现同图);`pool_case_d_test.gd`/`feat_vouchers_test.gd` `first_script` 键清退(:37/:68/:113/:142/:160/:25)+`cross_system_flow_test.gd:79` 随动;`main.gd` 参数面断言(`meta_progression_forge_test.gd:316/:323-324` first_script extra 键样例换非 first_script 键,「入口特有键随 extra 原样并入」语义保持)。可核查结果:单测面全绿、全仓 `first_script`/`template_pick`/`active_template` 零引用(件核)。 -- [ ] 验收步骤:T 段出口跑一次全套闸,期望冒烟+单测全绿、探针段无红。 -- [ ] 变异验证:终点前驱改非店节点 → 期望商店断言①红;某路径加第二家店 → 期望③红;生成器夹具注一滴计数器依赖(同种子两次产图不同)→ 期望 S6① 红;`level.tres` 偷留一个 `templates` 键 → 期望 S6④ 红。记录表进实现报告。 - -### T4 产出物落位卡(≈0.5d,末卡——三份 `reports/` 文件写齐才算收口) - -- [ ] 轻闸全绿后、合并前写齐三份:`reports/iter-实现报告-<完成日>.md`(各卡状态+变异验证记录表+实机目检总清单+遗留读数义务)/`reports/iter-代拍板追认单-<完成日>.md`(§10 白名单内代拍项留痕)/`reports/iter-反哺点-<完成日>.md`(§6 六条每条一落点:①关卡节点地图.md 节点编号措辞、②冒险结构.md 覆盖义务措辞+括注注销、③关卡节点图生成.md 用语核对、④level-map.md 呈现面随动登记、⑤06 状态回写与条目记载、⑥roadmap 编入笔)。可核查结果:三文件在 `reports/` 且 §6 同步面每条有落点。 - -**估时合计 ≈6.5–8.5d**(T0 0.5–1+T1 1.5–2+T2 2–2.5+T3 2–2.5+T4 0.5)。切分预案:T1 可单独一轮(夹具驱动、不碰运行时);T2+T3 须同轮落地(切源与模板断言换代同拍,拆开中间态冒烟必红);超软盒(>9d)预挂:T3 冒烟面清单若超一孔,先落 S1–S6(模板退役/商店/确定性主链)保主链绿,S7/S8 的探针与单测随动次轮补,全清单绿才收口。 - -## 8. 验证闸(逐需求点验收清单) - -| # | 需求点 | 测什么(断言/行为) | 落点/判定手段 | -|---|---|---|---| -| V1 | 加载校验四检+良构 | 正向:多入口多终点图/含岔口图/单链图过校验;负向:非 Boss 终点、带出边 Boss、环(回边)、孤儿节点、断头节点、悬空出边、句柄重复、战斗节点缺 depth——各报错误且起关拒绝 | 单测 `level_table_validate_test.gd`(新建)+起关拒绝冒烟 | -| V2 | 多入口首跳 | 双入口图入口停点候选=2(全部入口),pick 任一正确进入;单入口行为逐位不变 | 单测(runner 注入图/生成器夹具参数驱动,形态以卡①接口定稿为准) | -| V3 | 大岔口行进与多 Boss 兑付收束 | 多终点生成图(L2/L3 参数档出口 >1 时大岔口分区域)走不同区域各兑付各自 Boss(走哪条路决定打谁——A 区路径不误兑付 B 区 Boss);任一 Boss 胜=`level_finished` 恰一次(result=victory);倒下=defeat;收束次序先清库后宣布(既有断言保持) | 冒烟(生成图驱动全程) | -| V4 | 单链退化合法+三关含岔口+大岔口自 L2 起 | 单链夹具图过校验(不报「必有岔口」类错误);L1/L2/L3 参数档生成图各含 ≥1 个岔口节点(出度 ≥2);L1 生成图终点恰 1(大岔口自 L2 起的图侧投影) | 单测+冒烟(生成图扫描) | -| V5 | 商店编排三条 | 每终点直接前驱恰一店;商店总数=终点数;所有入口→终点路径恰穿一店(DFS);三关参数档×种子扫描(42+range(42,62))全绿 | 冒烟(S3 换代断言) | -| V6 | 定位信息两样 | begin_battle 载荷 (level_index, node_depth) 与图上节点 depth 一致、enc 键名零变化;与卡③三参解析衔接(敌侧行集命中) | 冒烟+单测载荷断言 | -| V7 | 确定性承载 | 同 (种子, 参数档) 两次产图逐位同图;首局种子 42 两次开局同图;断点存→恢复→再产图同图 | 冒烟(S6 ①②③) | -| V8 | 模板 0 语义废除+旧断点代差 | `level.tres` 无 `templates` 键;全仓零 `first_script`/`template_pick`/`active_template` 引用;`is_first_adventure_done` 种子选择两态行为不变(42/随机);缺代差标记键的旧断点被显式拒绝续玩(禁静默回放) | 冒烟(S6④+收束置位改写断言)+单测(旧断点拒绝——`breakpoint_resume_test`)+件核 | -| V9 | 15 张模板清理 | 三行 `templates` 键不在;模板池代码路径(`_resolve_template_row`/模板池校验分支/`_level_active_nodes` 模板读取)零残留;表尾去向注释在位 | 冒烟表断言+件核(git diff 仅删模板段) | -| V10 | 反哺点清单 | §6 六条每条有落点 | 件核对表(T4) | -| V11 | 生成图覆盖+多终点 Boss 带互异(消费侧绿灯断言,S5 承接跨表校验④语义) | 生成图每个 battle/boss 节点深度 `band_of` 非 -1(禁顶 clamp);多终点各深度解析带键两两互异(共带即红);板侧解析命中非空——以 §0 硬交付序列为绿灯前提 | S5 断言(三关参数档×种子扫描,宿主 `level_shop_economy_test.gd` 换代段) | - -### 8.1 断言面随动清单(S 清单——模板时代遗留、须换代的断言/直呼面;宿主 2026-10-24 v1.1 重锚=批 B 迁入位) - -| # | 断言/直呼面 | 位置 | 换代内容 | -|---|---|---|---| -| S1 | 模板池断言段 `_smoke_node_map_templates` | `game/tests/integration/node_map_templates_test.gd:32-104` | ①-⑥ 退役(node_map 标签/池=5/模板 0=现行/强制 0/N=1 回滚档/first_script 分支);⑦ 收束置位保留改写(语义换首局种子) | -| S2 | 模板感知取节点表 `_level_active_nodes` 三宿主副本及消费点 | `level_shop_economy_test.gd:970`(消费 :102/:105/:211-219/:260-274/:689/:698);`adventure_feats_test.gd:812-816`(消费 :118-130);`round_flow_skill_pool_test.gd:357`(消费 :319/:330) | 改经卡①接口取生成图(固定种子+参数档) | -| S3 | 商店图不变式 `_smoke_shop_graph`+行进到商店房断言 | `level_shop_economy_test.gd:679-715`/:601 | 换三条性质断言(终点前恰一店/商店数=终点数/全路径恰穿一店);硬编码 8/9/穿节点 8/L1 行集零改动退役 | -| S4 | 形状写死断言 | `level_shop_economy_test.gd:102`/:257(节点数=10)/:714(L1 行集零改动)/:105-110、:211-219、:260-274(逐节点扫描) | 节点数改生成图读数性质;L1 单链断言退役;扫描断言敌侧键面随卡③ S 清单、本卡换取数面 | -| S5 | 覆盖义务断言(en_keys 键聚合→生成图覆盖) | `level_shop_economy_test.gd:211-219/:267-274`(跨表行完整)/:704-714(商店房跨表行集)/`adventure_feats_test.gd:118-130` | 节点源换生成图;敌侧判据=`band_of` 非 -1(禁顶 clamp)+多终点 Boss 解析带键互异(承跨表校验④/迭代 34 检查语义);板侧=解析命中非空;以 §0 数据覆盖为绿灯前提 | -| S6 | 确定性断言(新增) | `integration/node_map_templates_test.gd` 换代段(S1 同宿主) | 同图三断言+`level.tres` 无 templates 键(V7/V8 承载) | -| S7 | 驱动/工具面 | `game/sim/level_autoplay.gd:37/:63/:141/:200-202/:12/:377-379`;`game/sim/adventure_monte_carlo.gd:139`;`game/probe/shot_probe.gd:12-13`;`game/probe/ui_click_probe.gd:373` | template_pick 尾参退役;MC 字段 `level_templates`→`level_maps`;route 轴句柄序;探针假设性质化 | -| S8 | 单测随动面 | `breakpoint_resume_test.gd`;`pool_case_d_test.gd:37/:68/:113/:142/:160`;`feat_vouchers_test.gd:25`;`cross_system_flow_test.gd:79`;`meta_progression_forge_test.gd:316/:323-324`(参数面 extra 键样例);`config_db_cross_validator_test.gd`(`_test_level_node_band_rules` 段,:19 调用行) | 断点键换代断言;first_script 键清退;extra 键样例换非 first_script 键(断言语义保持);④ 静态检查段随 T2 退役(语义由 S5 承接) | - -清单口径:S1–S6 归 T3 断言面,S7 归 T3 驱动面,S8 归 T2/T3 单测面;「冒烟」措辞沿 v1.0 语义=断言面统称,批 B 后其载体已是 `game/tests/` 各宿主(非 `game/smoke/`——该目录已退役);`tools/` 下历史生成脚本不随动(§5 不做)。 - -### 8.2 改动—验收对照表 - -> 六格齐(test-conventions §9.3);层级列只填 §9.2 四层词(单元/接口/集成/探针),件核是证明方式不是层级;T0 为事实坐实卡(无行为改动),其红证并入 V1(加载判据事实面)与 V8(模板链路事实面);纯产出物行按 §9.1 标「非行为改动」。 - -| 需求点 | 改动 | 验收测试 | 层级 | 真实入口 | 撤销或变异证明 | -|---|---|---|---|---|---| -| 加载校验四检+良构 | `level_table.gd` 新增 `validate_graph`、旧六判据与模板池校验分支退役 | V1(`level_table_validate_test.gd` 正负向夹具+起关拒绝冒烟) | 接口+集成 | 起关真实入口 `LevelRunner.start_level_external`(违规图返回 false) | 变异:注释「终点恒为 Boss」判据→负向红;无环判据短路→回边红 | -| 行进语义(大岔口/多 Boss/单链) | `level_runner.gd` 多入口首跳+句柄寻址+单链显式出边;收束泛化保持 | V2(双入口候选断言)/V3(多终点全程驱动)/V4(单链夹具+含岔口+L1 终点恰 1) | 单元+集成 | 冒烟生成图全程驱动(`level_autoplay.run_level` 真实入口)+runner 夹具单测 | 变异:候选写死单入口→V2 红 | -| 商店编排冒烟换代 | `smoke_all.gd` `_smoke_shop_graph` 三条性质断言换代 | V5(终点前恰一店/商店数=终点数/全路径恰穿一店,种子扫描) | 集成 | 冒烟商店段(`python tools/run_smoke.py`) | 变异:终点前驱改非店→①红;路径加第二店→③红 | -| 生成图覆盖+Boss 带互异(S5 新判据,承跨表校验④语义) | S5 换代断言:`band_of` 非 -1+多终点带键互异+板侧命中(落 `level_shop_economy_test.gd` 换代段) | V11(S5 覆盖与带互异断言,种子扫描) | 集成 | 生成图逐节点/逐终点解析驱动(`resolve_enemy_rowset`/`band_of` 真实消费链) | 变异:enemy.tres boss 带并成单带→带互异断言红;删某 (关,row_kind) 顶带→覆盖断言红 | -| 定位信息两样 | `begin_battle` enc 值语义换代(node_depth=深度)+句柄寻址 | V6(载荷断言+卡③解析衔接) | 接口 | 战斗真实入口 `begin_battle`(冒烟驱动开战) | 变异:enc node_depth 取句柄→V6 红 | -| 确定性承载+旧断点代差 | `adventure_runner.gd`/`main.gd`/`save_manager.gd` 模板链路退役+断点键换代+代差标记键 | V7(S6 同图三断言)/V8(模板键零引用+旧断点拒绝) | 接口+集成 | 冒烟 S6+`breakpoint_resume_test.gd`+续玩真实入口 `resume_adventure` | 变异:生成器注计数器依赖→V7 红;旧断点放行回放→V8 红 | -| 15 张模板清理 | `level.tres` 三行 `templates` 键删除+模板池代码路径清退 | V9(表断言+件核) | 接口 | 冒烟表校验段+`git diff` 件核(仅删模板段) | 变异:留一个 `templates` 键→V9 红 | -| 反哺点清单(**非行为改动**——纯文档产出物,无游戏行为,§9.1) | §6 六条落 `reports/iter-反哺点-<完成日>.md` | V10(件核对表) | 非行为改动 | T4 产出物件核 | 撤销证明:缺任一落点→V10 件核红 | - -**旧断言预声明(test-conventions §9.5)**——允许改写的旧断言清单及理由:①§8.1 S1–S8 随动面全部条目(理由:图源换模板池→参数档生成、寻址换句柄与节点深度、断点键换代;其中标注「断言语义保持」者只换键样例/定位方式、判据不动);②V5 商店三条性质断言换代(理由:商店编排规则由「每关恰 1·Boss 前一位」换代为「每终点前一位恰一店」,旧断言判据对象已废);③V8 收束置位改写断言(理由:模板 0 回滚档语义废除,置位条件随种子承载换代);④**跨表校验④静态检查退役**(`config_db_cross_validator.gd:54-106` 含迭代 34「同图多终点 Boss 解析带键互异」:103-106 与顶 clamp 拦截 :90-102,及 `config_db_cross_validator_test.gd` `_test_level_node_band_rules` 段——理由:检查对象=表内静态节点,切源后随模板数据消亡,留置即死代码;两条拦截语义由 S5/V11 运行时断言承接,拦截面从静态代理升级到真实生成图,无削弱)。除此之外的断言删除/放宽/换下层直调=未声明削弱,停卡呈报、独立审计判红。 - -**独立验收审计**(test-conventions §9.7):合并前由未参与实现的会话或子代理执行——审计入口=只读需求卡片+本对照表+diff+测试文件+实现报告,自跑最窄测试抽查(不采信「已全绿」自述);审计报告文件=`reports/iter-验收审计-<日期>.md`,逐行写「通过/缺漏/例外及依据」,未通过不得合并。测试执行两入口铁规不变(审计在迭代 loop 内启动的子代理算 loop;另开会话须用户点名)。 - -**测试方式两分法(2026-10-11 用户定)**:V1–V10 全部机检自动化(冒烟+单测);能力验证可经调试接口直改种子/参数档驱动生成图(沿用现役 DebugOverlay/DS_DEBUG 门控)。**用户目检项**:「三关实机选路观感不回归(多入口首跳/大岔口取舍/多终点走向可见)」+「首局教学图体验(42 种子固定图)」两条——观测项写进 `reports/iter-实现报告-<完成日>.md` 实机目检总清单,登记不阻塞(验收时间用户自行安排)。 - -**闸口径**:轻闸每任务卡出口自跑;批末重闸=`tools/run_all_gates.sh` 按改动域选档(本批只碰 `game/` 代码与数据表 → 代码域档)全绿才合并+改前改后读数对拍。回归措辞:里程碑步(T 段)出口跑一次全套闸;波内修复只做出红最小单元的最窄确认;新增测试先行孤立验证再并入全套。 - -**变异验证伴生**(新增/改写验收函数逐条注入实验,记录表进实现报告):终点恒 Boss 判据注释(V1 红)/无环判据短路(V1 红)/多入口候选写死单入口(V2 红)/终点前驱改非店(V5 红)/路径加第二店(V5 红)/enc node_depth 取句柄(V6 红)/生成器注计数器依赖(V7 红)/旧断点缺代差键却放行回放(V8 红)/level.tres 留 templates 键(V9 红)。变异不红则改判据或如实标注拦截边界。 - -## 9. 异常分级 - -- **L1(自救)**:断言措辞、命名、注释、消息文本;冒烟消息随语义换代更新。 -- **L2(记录继续)**:图数据内部适配形态(句柄载体/生成图在 runner 内的持有结构);校验实现形态(深度单调判据的实现写法);冒烟断言定位写法(生成图直取 vs 经 runner 读面);测试驱动形态(注入图 vs 生成器夹具参数);route 轴/MC 字段命名细节(§10 ⑤)。 -- **L3(停机呈报)**:卡①接口与两条硬语义(同图/纯函数复现)不兼容且超出适配面;执行中发现同深度多终点图样(推论条款 A 被破,契约 8);任何触碰定位信息两样契约、收束次序、商店编排语义的诉求;生成图深度域超出敌/板表覆盖且数据批无人接手(§0 硬交付序列失管);模板清理出现残留被外部引用且本卡兜不住;校验四检判据被要求放宽(违 D1:51「加载期拦截」)。 - -## 10. 代拍板白名单(超清单必呈报) - -1. 图数据内部适配形态(节点句柄载体、生成图在 runner 内的持有结构、`generate` 返回值的内部解析)——约束:读面 API 签名不变、句柄唯一、禁 node_depth 兼身份(契约 2)。 -2. 校验实现形态(无环判据的深度单调写法 vs DFS 判环——契约 1 语义内)与校验错误类别命名(沿 `structural`/`dangling_next`/`dup_key`/`enum`/`domain` 词表,确需新类别时命名留痕)。 -3. 断点键换代细节(代差标记键命名、退役键清单的缺键容忍、句柄字段的断点序列化形态、旧断点拒绝后的盘面处置〔清/留〕)——约束:save 域扩列不升版先例、复现同图不破、旧断点禁静默回放(契约 3)。 -4. 冒烟断言定位写法(生成图直取 vs 经 runner 读面)与断言名措辞——约束:禁写死节点编号形状(契约 4)。 -5. `level_autoplay` route 轴实现(句柄升/降序的取数写法)与 MC 字段名 `level_maps` 的值形态(种子+参数档标识)。 -6. 提交切分与 worktree 分支命名。 - -## 11. 待裁点 - -**零待裁点,开工门=U0。** - -四处灰色地带已按设计事实推导定值(用户可否决,随 T4 反哺点清单呈报可见): - -- **确定性复现机制(契约 3)**:备选「断点存生成图快照」——淘汰依据=D3:264「固定图由此(固定种子+固定参数档)承载」明文,存图快照不是「由种子+参数档承载」;备选「统一流号位重放」——淘汰依据=复现依赖全局计数器位次,与 D2 执行边界「同种子+同参数档产出同一张图」冲突(同参数档不同位次会漂);定值=生成图=(种子, 参数档) 纯函数,断点零新增图身份键,续玩以主种子+参数档重生成同图。依据=D3:264+D2:60 合取。 -- **U2 两规则退役(单链前三纯战斗/层宽 ≤5)**:备选「保留作生成图加载校验」——淘汰依据=两规则的判据对象是旧模板形状(`level.tres` 表尾 U2 返工注记在案),D2 v1.1 产出规格无此约束(「宽度=分叉生长」新语义无层宽上限;「前三纯战斗」在含岔口图上不可定义);备选「移交卡①产出约束」——淘汰依据=产出规格五条已锁清单无此两条,扩产出规格归文档批非本卡;定值=随模板池退役,教学担子由首局种子 42 固定图+难度曲线数值责任承载(D1:120「教学位节点同样掷骰、没有结构豁免」)。依据=D2 产出规格+用户卡②范围句「规则语义以生成文档产出规格为准」。 -- **多终点深度互异不入本卡校验(契约 8)**:备选「消费侧加载校验加第五检」——淘汰依据=用户范围句明列四检「勿扩勿缩」,且卡③ §0 已把图侧兑现归口卡①产出规格;定值=本卡不新增校验、只留 L3 呈报口(同卡③契约 6 口径)。依据=用户范围句+卡③契约 6。 -- **旧断点代差处置(契约 3 代差句)**:备选「旧断点静默按新语义回放」——淘汰依据=旧 `node_id` 值与新句柄空间可碰撞错位(静默错节点=违反 fail-fast);备选「迁移旧断点」——淘汰依据=旧模板图已随模板池退役、旧 node_id 指向的图不复存在,无迁移动作可做(禁凭空捏图=无兜底图红线);定值=扩列代差标记键、缺键旧断点显式拒绝续玩(返回 false+push_error),盘面处置(清/留)落 §10 ③。依据=AGENTS.md fail-fast 红线+D2 无兜底图+迭代 21 T5 开发期破坏性先例。 - ---- - -**自审五查记录(2026-10-21)**:①规格覆盖——§1 六件目标/八项完成定义逐条挂 T 卡(校验=T1、行进/定位/确定性=T2、商店冒烟/模板清理=T3、反哺点=T4;确定性横跨 T2 链路退役+T3 断言补齐),无漏项;②步骤扫描——全部步骤一步一动作一可核查结果,实现体只给签名+判据名(`validate_graph` 判据清单、商店三性质断言、确定性三断言),无抄写体;③类型一致性——接口块签名与 T 卡引用逐字一致(`generate(level_index, params, seed)`/`validate_graph(nodes)`/enc 三键/runner 读面七个方法名);④审查焦点——补挂四类失败模式:违规图放行(V1 变异)、商店位漏店/双店(V5 变异)、复现漂移(V7 变异)、模板键残留(V9 变异);⑤篇幅比例——规格约 1 屏、任务卡步骤与实现面 1:1,函数体零展开。 - -**v1.1 修订自审(2026-10-24,宿主重锚批——六查复核)**:动笔面=§3 F1–F10 宿主与行号重锚(smoke_all 五处指认全迁批 B 宿主:S1→integration/node_map_templates_test、S3/S4/S5 主体→unit/core/level_shop_economy_test、extra 键样例→meta_progression_forge_test、驱动面→sim/probe 新路径)+§0 硬交付序列补 2026-10-24 数据实况与 Boss 带互异条款+T1/T2/T3 行号订正+T1 补 manifest 登记步(阻断 4 闭合)+**T2 跨表校验删除范围重划**(阻断 3 闭合:④ 整段 :54-106 随静态节点数据消亡退役,迭代 34 检查〔:103-106〕的拦截语义由 S5/V11 运行时断言承接——原 v1.0 写「只删死分支」时迭代 34 尚未在该段落码,照旧执行会连带静默删掉已交付断言;拦截面从静态代理升级到真实生成图,无削弱,入旧断言预声明④)+§8.1 S 清单宿主换代+§8/8.2 补 V11 行与变异证明+归据句卡③④ 状态换代。六查:①规格覆盖——设计面零变动(§1/§2/§4 契约未触;§4 契约 8「本卡不新增校验」不受影响——S5 是断言不是加载校验),V11 为既有 §0 交付序列的验收落位(v1.0 缺行,本轮补齐使验收闭合);②步骤扫描——manifest 步与 S5 语义承接步均一步一动作+可核查结果;③类型一致性——签名零变动(卡① 接口块期望签名沿 v1.0 原文,T0 对齐步不变);④改动覆盖——V11 新行带撤销/变异证明(boss 带并单带→红;删顶带→红);⑤篇幅比例——零函数体;⑥落位复用——断言宿主全复用批 B 16 宿主与既有 manifest 行体例,S6 落 S1 同宿主零新增登记。重锚依据=`reports/批B-断言迁移账本-2026-10-22.md`+2026-10-24 master 实查(16c6f136/迭代 34 落地后现态)。 - -**grilling 复核记录(2026-10-21,设计树对抗复核,不采访用户——可推导定值写依据、真归用户列 §11)**:按设计树走查八支——①校验语义边界(四检 vs 产出规格五条:五条是卡①产出侧自证〔D2:117 产出合格判据〕、四检是消费侧加载拦截〔D1:51/:176 明列清单〕,「个数=参数档」括注归产出侧、本卡不核个数——边界写进契约 1①);②节点深度歧义(合流/多入口下 depth 赋值一致性归卡①产出规格,消费侧以「出边目标深度严格大于源」兜前进方向);③多入口路径语义(「每条路径恰好从一个入口出发」=每个节点在某条入口→终点路径上〔无孤儿无断头〕+入口为无入边节点——形式化写进契约 1②);④确定性复现(三案淘汰两案、定值纯函数复现,§11 留否决口;RNG 机制协调归卡①接口定稿、硬语义不浮动);⑤U2 规则去留(判据对象消亡+产出规格无据 → 退役定值,§11 留否决口);⑥商店「前一位」形式化(D1:63「前一位恰一家店」+D1:182「恰穿一家」+D2「商店数=出口数」三者合取 ⇒ 终点入边恰一且前驱是店——契约 4 表述核对);⑦**旧断点代差(本轮新查出的真缺口)**——旧断点 node_id 与新句柄空间可碰撞静默错位,定值代差标记键+显式拒绝(§11 留否决口);⑧跨卡序列(生成图深度域 vs 敌侧带覆盖=真实缺口,卡③ §5 已登记归口——本卡把交付序列写死为 §0 硬前置而非待裁)。code-conventions 对照:§2.2「跨层标识符语义单一且成文」恰为契约 2 的规范依据(node_depth 双语义是现役违规态)、S9 类型闸与既有 `validate()` 同形、§4.11 新单测不经 autoload 无冲突。AGENTS.md 硬红线逐条过:数值单点(本卡零数值、参数档只读)、RNG 单点(生成器取数归卡①、本卡不自建 RNG)、测试两入口(T 卡只写断言不跑测)、实现批禁写 design/(§6 义务全走反哺点、零旧口径义务措辞)、fail-fast(违规图致命无软回退+旧断点显式拒绝)。结论:零待裁点,四处灰色推导已定值并留 §11 供用户否决。 diff --git a/reports/需求卡片-节点图生成器核心批-2026-10-21.md b/reports/需求卡片-节点图生成器核心批-2026-10-21.md deleted file mode 100644 index e1dc6e9e..00000000 --- a/reports/需求卡片-节点图生成器核心批-2026-10-21.md +++ /dev/null @@ -1,268 +0,0 @@ -# 需求卡片:节点图生成器核心批(参数化长图生成器)· 关卡节点图重构卡① - -- 版本:**v1.1**(2026-10-24 修订重锚——§3 行号与断言宿主对齐 game/smoke/ 退役〔16c6f136〕后仓库现态+run_all manifest 登记步补齐;v1.0 2026-10-21 成文,自审五查+grilling 复核结论见文末 §11) -- 来源:[待办清单](../待办清单.md)「关卡节点图重构——可选节点太少」(2026-10-13 登记,来源=playnote,设计面=文档轨,状态=文档设计已闭合)(**唯一事实源**:[关卡节点图生成](../design/gdd/systems/冒险循环/关卡节点图生成.md) **v1.1**(2026-10-21 双定稿闭合凭据之一);协作凭据=[关卡节点地图](../design/gdd/systems/冒险循环/关卡节点地图.md) v2.1+[冒险结构](../design/gdd/systems/冒险循环/冒险结构.md) 关卡行集/结构冻结/覆盖义务行) -- 归属:关卡节点图**生成侧**(参数化长图生成器算法本体+五旋钮参数档数据表+产出约束自证+执行边界+确定性承载);关卡节点图重构批**卡①**(卡②消费侧换代/卡③敌人表深度带〔已落地·迭代 34〕/卡④迷雾可见性〔已落地·迭代 34〕同批) -- 排期:未编入(2026-10-18 用户令:需求卡片不预占迭代号,批次归属按工作量另裁) -- 开工门:U0(用户 `/iter` 发令)+ grilling 复核门过(零待裁点,见 §11) - -## 0. 前置依赖与并行性 - -- **设计闭合(第〇步)**:06 条目「关卡节点图重构——可选节点太少」状态=文档设计已闭合,凭据=关卡节点图生成.md v1.1+关卡节点地图.md v2.1(各带路径+版本号)→ 放行写卡。 -- **本卡是生成侧,被卡②硬依赖**:卡②(消费侧换代)运行时图来源换成生成器产图,其契约 7/接口块明文「签名与图数据形状**以卡①定稿为准**」;本卡 §7 接口块即该定稿,卡② T0 按本卡对齐(产图签名/图数据形状/参数档 schema 字段名/复现语义四件)。**并行无阻塞**:本卡不依赖卡②实现,自证产出合法图即可独立验收。 -- **输出推论条款 A 给卡③**:卡③(敌人表深度带)契约 6 推论条款 A「多终点深度互异」的**图侧兑现归本卡产出规格**(卡③ §6 反哺点③明文派发);本卡把「多终点落在互异节点深度」定为产出约束(§4 契约 3/§8 V6)。 -- **RNG/复现机制由本卡定稿**:卡② 契约 3 把「纯函数复现与统一流的机制协调(derive_at 纯派生/号位内重放/RngService 纯流面)归卡①接口定稿,硬语义不变」——本卡 §4 契约 1 定值为 RngService 纯流面(推导链见 §11)。 -- **深度赋值语义由本卡定稿**:卡② 契约 1④/grilling 注记「合流/多入口下 depth 赋值一致性归卡①产出规格」——本卡定值为「自入口最长路径步数」(§4 契约 2,推导链见 §11)。 -- **生成图深度扩面的敌/板表覆盖(遭遇坐标覆盖)不在本卡**:归卡③ §5 已登记的「生成图深度扩面覆盖问题(卡①/后续表批)」中的**后续表批**——本卡只产出深度值并登记交付序列依赖(§5 不做/§6 反哺点),敌/板表补行是数值/敌人表(卡③/后续表批),本卡零敌数值(用户范围句明示「敌人表深度带(卡③)」不做)。 - -## 1. 目标与完成定义 - -**目标**:把关卡节点图生成 v1.1 的「参数化长图生成器」落成一个**纯函数生成器**——吃「参数档+种子」产出一张合法节点图,交付给消费侧(卡②)。四件本体:① 五旋钮(层深/分支密度/配比/入口个数/出口个数)+ L1/L2/L3 参数档数据表(schema+.tres,provisional 值);② 产出约束五条+结构铁律(生成器产出自证);③ 摆放约束三条(商店位/岔路取舍·下一跳/岔路取舍·支路构成);④ 执行边界四条(不合格禁止进玩法/无兜底图/参数无解即 fail-fast/同种子+同参数档=同图、随机只经 RngService)。 - -**相对现状,新增了什么**(现状=固定模板池,见 §3): -1. 运行时参数化生成器:图从「固定 15 张模板池」换成「(参数档, 种子) 纯函数生成」——改形态=改参数不改代码; -2. 五旋钮参数档数据表(schema+.tres,L1/L2/L3 三档,provisional 值)+ ConfigDB 注册与单点读取; -3. 产出约束/结构铁律/摆放约束的**产出侧自证**(生成器交付前强制过一遍,不过即参数无解致命); -4. 确定性承载:同(种子, 参数档)⇒ 逐位同图,纯函数复现、复现不依赖统一流全局计数器号位(RngService 纯流面); -5. 多入口多终点/每路径恰一入一出/大岔口自 L2 起(L1 出口恒 1)/多终点深度互异 的图侧兑现。 - -**完成定义**(全部满足才算本批完成): -1. `generate(params, seed)` 产出图**必过**产出约束五条+结构铁律+摆放约束三条(生成器产出自证在位,自证失败=参数无解致命中止); -2. 参数档数据表(schema+.tres)落地 ConfigDB 单点读取,L1/L2/L3 五旋钮齐备(provisional 值,域校验通过);生成器只吃参数档、不认关卡号; -3. 确定性:同(种子, 参数档)两次产图逐位同图;复现不依赖统一流计数器号位(counter 无关);随机只经 RngService(纯流面),零 `randomize()`/零自建 RandomNumberGenerator; -4. 生长递增:L1/L2/L3 读数(最长路径步数、节点总数)逐关递增,且节点总数增幅 Δ(L2→L3)>Δ(L1→L2); -5. 专项兑现:L1 出口恒 1、大岔口(多终点归属锁定)自 L2 起;任意岔口各去路下一节点类型互不相同、各支路构成互不相同;每条入口→终点路径恰过一家店;多终点落在互异节点深度;战斗节点数 ≥ 保底数; -6. 参数无解(给不出合法图)=配置错误,致命中止(fail-fast),无兜底图、无软回退; -7. §8 验收断言批全绿+变异验证记录表进实现报告; -8. 批末重闸按改动域选档全绿+改前改后读数对拍;三份 `reports/` 产出物写齐(T4)。 - -## 2. 设计事实源 - -| # | 出处(**唯一事实源**) | 取用条款 | -|---|---|---| -| D1 | `design/gdd/systems/冒险循环/关卡节点图生成.md` **v1.1**(2026-10-21,06 闭合凭据之一——本卡 100% 以它为唯一设计依据) | :41-49 产出约束五条(道路系统=除岔口〔≥2 去路〕外每节点恰一条去路、岔口去路 2–3/宽度=分叉生长〔不摆平行层、同深度节点数由岔口自然长出 1→2→3→4〕/层高=沿路步数〔入口到终点最长路径步数、L1≥6、逐关递增且增幅变大〕/出口归属锁定〔大岔口分成互不连达区域、各区域只通向自己的终点、终点前一层去路集合恰含一个终点〕/战斗节点保底);:51 结构铁律(连线只朝前进方向无环/入口终点可多个〔个数=参数档〕/每条路径恰从一入口出发恰到一终点);:53-60 执行边界四条(不合格产出禁止进玩法/无兜底图〔软回退禁〕/参数无解即致命中止/同种子+同参数档产出同一张图、随机只经 RngService 统一流);:62-72 五旋钮与三套参数档(层深/分支密度/配比/入口个数/出口个数;**生成器只吃参数档、不认关卡号**;生长曲线层高宽度节点数路径长度逐关递增且增幅变大、决策点密度逐关递增、第一关即含岔口;单链是分支密度取下限合法退化形态、不作为三关取值;大岔口自第二关起、第一关出口恒 1);:74-88 配比与摆放约束三条(商店位=每终点前一位恰一店/岔路取舍·下一跳=一岔口各去路下一节点类型互不相同、下一跳必在可见范围/岔路取舍·支路构成=一岔口各支路节点类型构成互不相同、每支路至少一个别的支路没有的类型)+扩展节点类型位;:86 内容归 owner(战斗打谁=敌人系统深度带配池);:101-115 调参表(层深 L1≥6 已锁/L2>L1 增幅变大 provisional/岔口去路 2–3 已锁/分支密度 provisional〔一局十几次〕/入口 1 起 provisional/出口 L1 恒 1、L2/L3 1–3 已锁范围/配比 provisional/战斗保底数 provisional/商店数=出口数已锁);:117-128 验收标准(产出合格/生长递增〔L1/L2/L3 最长路径步数与节点总数递增、Δ(L2→L3)>Δ(L1→L2)〕/归属锁定/必经恰一店/岔路取舍/可复现/无软回退/战斗保底) | -| D2 | `design/gdd/systems/冒险循环/关卡节点地图.md` v2.1(2026-10-21,06 闭合凭据之一——走图视角语义参照,本卡不实现走图) | :51 结构铁律走图三句(无环/每路径恰一入一出/终点恒为 Boss 可多个)+「规则语义以关卡节点图生成的产出规格为准」;:57-61 五类节点(battle/boss/reward/rest/shop,Boss 恒为终点);:65 节点深度=自入口步数、遭遇坐标=关卡序号+节点深度、终点恒在各路径最深处、单链合法退化、三关参数档都含岔口;:63 商店编排(每终点前一位恰一店、每局必经恰一店、位置约束由冒烟断言把关非加载校验);:103 下一跳必在可见范围(可见性规则归卡④);:119 定位信息只有两样(关卡序号+节点深度) | -| D3 | `design/gdd/systems/冒险循环/冒险结构.md`(状态:已定稿) | :264 关卡行集行(关卡表逐关行承载关数与图的参数档;「旧模板池随实现批清数据」;「**确定性 = 固定种子+固定参数档(首局教学与存档复现的固定图由此承载,无兜底模板)**」);:275 结构冻结(终点恒为 Boss〔终点可多个、各在路径最深处〕、一冒险一主种子、收束点先清库后宣布);:293 覆盖义务(行集内每个战斗与关底节点〔关,深〕在敌人与棋盘生成的数值表中有行——**措辞随深度带换代**,敌/板表覆盖归卡③/后续表批) | - -闭合凭据一致性:D1 与 06 条目状态内列的设计定稿逐字一致(`关卡节点图生成.md` v1.1);D2/D3 为协作语义参照,与 D1 同向(确定性、结构铁律、覆盖义务)。 - -## 3. 现状事实(2026-10-21 成文实查;行号 2026-10-24 v1.1 重锚——对齐 game/smoke/ 退役〔16c6f136〕与迭代 34 落地后 master 现态;开工日 T0 复核行号) - -| # | 事实 | 出处 | -|---|---|---| -| F1 | **无运行时参数化生成器**;图来自固定模板池(15 张=3 关×5 张,L1 五张=单链排列变体 7 节点、L2/L3 网状 10 节点 node_depth 0–9);离线工具 `tools/t5_build_templates.py`/`t17_fanout2_rebuild.py` 一次性预生成入库(非运行时生成) | 模板数据 `game/data/tables/level.tres:8`(L1 五链)/:9(L2 五网)/:10(L3 五网);表尾生成史注释 `level.tres:13-19`(:19「生成器 = tools/t5_build_templates.py」);离线工具 `tools/t5_build_templates.py`/`tools/t17_fanout2_rebuild.py` 在盘 | -| F2 | 节点图数据结构=`level_row.nodes: Array[Dictionary]`,节点=`{node_depth, type, next?}`;图形态=任一节点带 `next`(`_graph_mode` 探测);节点 id 双编号空间(图=`node_depth` 当节点身份/单链=数组下标,注记「混用两空间会碰撞」);入口取「无入边**唯一**节点」 | 结构与探测 `game/core/level_runner.gd:67`/:149-158/:202-205;`_node_id` 双空间 `level_runner.gd:341-347`;`_entry_node` 唯一 `level_runner.gd:348-358`;出边 `_out_ids`/`_out_nodes` `level_runner.gd:360-385` | -| F3 | 现行图合法性校验约束与 D1 v1.1 冲突(旧模板形状约束):出度 ≤2(:133-138)、入口唯一〔无入边恰一〕(:139-149)、Boss 唯一终点+无出边+编号最大(:150-157)、Boss 计数恰一(:83-84)、单链末 Boss(:87-90)、单链前三纯战斗 U2(:94-98)、BFS 层宽 ≤5 U2(:183-201)——D1 v1.1 多入口多终点/岔口去路 2–3/大岔口自 L2 起 与之冲突;换代归卡②(消费侧加载校验),本卡产出侧按 D1 v1.1 | 校验体 `game/data/schemas/level_table.gd:83-84`/:87-90/:94-98/:133-138/:139-149/:150-157/:183-201 | -| F4 | 模板抽取经 `node_map` 流(`rng.open_stream("node_map")` 抽签)/`template_pick` 强制取模板;`begin_battle` 组装 enc=`level_index`+`node_depth`(值实为节点编号)+`node_type` | 抽取 `level_runner.gd:307-324`(:317-318 抽签、:314-315 强制);enc 组装 `level_runner.gd:513-525`(:514/:524) | -| F5 | RngService 统一随机流单点(禁 `randomize()`/自建 RNG):`open_stream(purpose)` 经全局计数器取号(计数器 +1)、`derive_at(scope)` 纯派生不推计数器、`PURPOSE_TAGS` 含 `node_map`;快照契约=主种子+计数器 | 类头纪律 `game/core/rng_service.gd:3`;`PURPOSE_TAGS` :9;`derive_at` :34-37;`set_master_seed` :52;`new_adventure` :67;`open_stream` :86-99;AGENTS.md:73「RNG 单点——禁 randomize()/自建 RandomNumberGenerator,随机只经 RngService 统一流」 | -| F6 | ConfigDB 数据表注册模式:`_tables` 映射表名→.tres、`var level: LevelTable`、`load_tables` 装载、访问器单点读取(`level_row(level_index)`);数值单一事实源=`data/tables/*.tres` 经 ConfigDB 单点读 | 注册映射 `game/core/config_db.gd:20`;成员 `config_db.gd:40`;装载 `config_db.gd:142`;访问器 `config_db.gd:245-246`;AGENTS.md:72 数值单一事实源 | -| F7 | 数据表 schema 范例(`rows: Array[Dictionary]`+`validate()` 域校验,空表=合法态):难度表 schema 同构可循 | `game/data/schemas/difficulty_table.gd:7`(rows)/:13-27(validate 域校验);同构参照 `game/data/schemas/level_table.gd:7/:21` | -| F8 | Core 层纪律:完整玩法须可 `--headless --script` 跑通、零 UI 依赖;`--script` 模式禁裸 autoload 标识符(ConfigDB/RngService 经节点路径注入) | AGENTS.md:75;`code-conventions.md:71`(`--script` 禁裸 autoload,经 `get_node("/root/…")`);`level_runner.gd:2-3`(Core 零 UI、headless 可跑) | - -## 4. 设计契约(红线,违反 = L3 停机呈报) - -🔴 **契约 1 · 生成器纯函数复现+RNG 单点(纯流面)**:`generate(params, seed)` 是 **(参数档, 种子) 的纯函数**——同(种子, 参数档)两次产图逐位同图,复现**不依赖**统一流全局计数器号位(D3:264「固定图由固定种子+固定参数档承载」+D1:60「同种子+同参数档产出同一张图」合取;断点续玩以「主种子+参数档」重生成同图、断点零新增图身份键——resume 在停点号位重生成,号位≠产图号位,故必须 counter 无关)。生成器取数**只经 RngService 纯流面**(新增 `pure_stream(seed_val, scope) -> RandomNumberGenerator` 或等价纯派生面:seed 键值、纯函数、**不推 `_counter`、不落事件日志**);**禁 `randomize()`/禁自建 RandomNumberGenerator/禁 `open_stream` 取数**(open_stream 推计数器=号位相关,破坏 counter 无关)。RNG 单点红线不破:随机仍出自 RngService(自建 RNG 才是红线违例,RngService 提供纯流面属服务内分工)。纯流面机制协调是本卡定稿(§11 推导),硬语义(纯函数/counter 无关/同图)不随机制实现浮动。 - -🔴 **契约 2 · 节点深度语义单一(id=句柄/depth=自入口步数)+多终点深度互异**:图数据节点字段拆两轴——`id: int`=图内唯一句柄(导航身份);`depth: int`=**自入口最长路径步数**(遭遇坐标取值,D2:65「节点深度=自入口步数」)。禁 `node_depth` 兼任身份与深度(code-conventions §2.2「位置/句柄/键三种不同、禁凭当前恰好相等传值」——现役 node_depth 双语义即违规态)。`depth` 赋值定值=**自入口最长路径步数**(多入口取「距任一入口最长路径」),此赋值使「出边目标 depth 严格大于源」成为有效无环判据(D1:51 无环+卡② 契约 1④)+与「层高=沿路最长步数」(D1:47)一致。**多终点深度互异**(推论条款 A,卡③ 契约 6 派发本卡兑现):多终点各落在互异 `depth`(同 depth 多终点在「定位信息只有两样〔关,深〕」下不可区分=设计不可能态,D2:119+D1:96「多终点各配 Boss 行」+「走哪条路决定打谁」合取);单终点(L1)不涉。 - -🔴 **契约 3 · 产出约束五条+结构铁律是产出规格唯一陈述处(产出自证)**:生成器交付前强制自证产出过 D1:41-51 全部——产出约束五条(道路系统〔除岔口外每节点恰一条去路、岔口去路 2–3〕/宽度=分叉生长〔不摆平行层、宽度源于岔口〕/层高=沿路步数〔L1≥6、逐关递增增幅变大〕/出口归属锁定〔大岔口分互不连达区域、各区域只通自己终点、终点前一层去路集合恰含一个终点〕/战斗节点保底)+结构铁律(无环〔连线只朝前进〕/入口终点可多个〔个数=参数档〕/每路径恰从一入口出发恰到一终点)。自证不过=**产出不合格**(执行边界①)。 - -🔴 **契约 4 · 执行边界四条(无兜底图、fail-fast)**:① 不合格产出禁止进玩法(自证不过的图绝不上给玩家);② 无兜底图(不设固定兜底图、软回退一律禁);③ 参数无解即配置错误、致命中止(fail-fast,push_error+assert/既有 fatal 路径,禁静默降级、禁空图/残图回退);④ 同种子+同参数档=同图(见契约 1)。禁任何「生成失败就用模板/用空图/用上一张」回退。 - -🔴 **契约 5 · 摆放约束三条**:① 商店位=每个终点前一位恰一家商店(每名玩家一局必经恰一家店)+商店总数=出口数(D1:115 已锁)+每条入口→终点路径恰过一家店;② 岔路取舍·下一跳=一个岔口分出的各去路,下一节点类型互不相同;③ 岔路取舍·支路构成=一个岔口分出的各支路,节点类型构成互不相同、每支路至少一个别的支路没有的类型。位置约束(①)由生成器产出侧兑现+冒烟/单测断言把关(D2:63「不由加载校验承载、由冒烟断言把关」——卡② 加载校验不查商店位,本卡产出侧必须产对)。 - -🔴 **契约 6 · 五旋钮+生成器只吃参数档不认关卡号**:生成器输入=参数档(五旋钮值)+种子,**不接收、不读取关卡序号**(D1:72「生成器只吃参数档、不认关卡号」);调用方(冒险结构/卡②)负责 level_index→参数档 解析后把参数档交给 `generate`。图=f(参数档, 种子),与第几关无关(同参数档+同种子⇒同图,即便来自不同关)。五旋钮=层深/分支密度/配比/入口个数/出口个数;配比四类占比合计 1、战斗保底优先;商店数=出口数(结构事实,非配比自由项)。 - -🔴 **契约 7 · 接口纪律与边界**:生成器产出图给卡②(§7 接口块,签名与图数据形状本卡定稿、两条硬语义〔同图/纯函数复现〕不随签名浮动);`depth` 喂遭遇坐标 (关,深) 交卡③ 带寻址(enc 键名 `level_index`/`node_depth`/`node_type` 保持,`node_depth` 值=`depth`);图结构只读读面供卡④ 可见性(可见性规则归卡④,本卡只保证类型数据在节点上)。本卡不实现生成算法之外的:走图/行进/兑付(卡②)、敌人表深度带与敌数值(卡③)、迷雾可见性(卡④)、参数档数值定值(定值批,数值全 provisional)。 - -## 5. 范围边界 - -**做**:参数化长图生成器本体(`generate(params, seed)` 产合法图);五旋钮参数档数据表(schema+.tres,L1/L2/L3,provisional 值)+ ConfigDB 注册单点读;产出约束五条+结构铁律+摆放约束三条的产出侧自证;执行边界四条(含 RNG 纯流面确定性承载、参数无解 fail-fast);验收断言批(产出合格/可复现/岔路取舍/必经恰一店/归属锁定/多终点深度互异/战斗保底/生长递增/L1 出口恒 1·大岔口 L2 起/参数无解)+变异验证设计。 - -**不做**(明示去向): -- 消费侧行进(加载校验换代、行进语义、句柄寻址、模板清理、确定性承载链路退役)→ **卡②**(已写卡);本卡只交付产图接口; -- 敌人表深度带(带切法、`resolve_enemy_rowset`、敌数值、多 Boss 行集)→ **卡③**(已写卡);本卡只产出 `depth` 值与多终点深度互异; -- 迷雾可见性(近处见类型/远处起雾、可见层数 N、Boss 标记不入雾)→ **卡④**(已写卡);本卡只保证节点 `type` 数据在节点上; -- **参数档数值定值**(层深/分支密度/配比/入口/出口/战斗保底的最终取值)→ 定值批落表(D1 调参表状态列),本卡 .tres 值全 **provisional**; -- **生成图深度扩面的敌/板表覆盖**(遭遇坐标覆盖义务:生成深度域须被敌侧深度带、板侧键域覆盖)→ 卡③/后续表批(§0);本卡只产出深度、登记交付序列依赖,零敌数值零板数值; -- 生成算法的具体长图步骤(怎么从旋钮长出一张合法图)→ 代码实现自由(D1:28「生成算法本身归代码——图怎么长出来是实现的事」);本卡只定产出规格与执行边界(WHAT,不写 HOW); -- 旧 15 张模板 `level.tres` 清理与模板池代码路径退役 → **卡②**(契约 6 模板清理义务);本卡不删模板数据; -- 呈现层(level-map 画布自适应、多终点标记画法)→ level-map/HUD 框架与卡④。 - -## 6. 关联文档同步面 - -> 本批是代码实现批,禁写 `design/` 整目录与根规则文件,唯一可写目录=`reports/`。下表「同步义务」一律执行为:逐条列进 `reports/iter-反哺点-<完成日>.md`(哪个文件哪一节、改成什么、依据),用户会话裁决后归文档批落文。同步义务一律走反哺点清单,禁用旧口径的「随文档批一并落」式措辞(2026-10-17 起作废)。 - -| 文档 | 关联点(本批实现会改变它的哪条陈述) | 同步义务 | -|---|---|---| -| `design/gdd/systems/冒险循环/关卡节点图生成.md` | 产出规格「多终点深度互异」条款缺位——本卡契约 2 兑现推论条款 A(卡③ 契约 6 图侧);「深度=自入口最长路径步数」赋值语义宜在图规格节成文(消 node_depth 双语义歧义) | 反哺点 · 列清单(①产出规格补「多终点落在互异节点深度」+「节点深度=自入口最长路径步数」赋值句,依据=D2:119+D1:96+卡③ 契约 6) | -| `design/gdd/systems/冒险循环/关卡节点地图.md` | :65「节点深度=从入口沿路走过来的步数」与「最长路径步数」在合流图上的赋值一致性宜钉成「自入口最长路径步数」(消歧) | 反哺点 · 列清单(②深度赋值消歧补注,依据=契约 2 推导;无歧义实现则记「无」) | -| `design/gdd/systems/冒险循环/冒险结构.md` | :293 覆盖义务措辞「精确行」随深度带换代(归卡③ 反哺点);:264「旧模板池随实现批清数据」归卡② 完成后注销(本卡不动模板数据) | 反哺点 · 列清单(③仅登记本卡产出深度域供覆盖核对,覆盖措辞归卡③ 落——本卡零改动) | -| `design/gdd/ux/level-map.md` | 图形态随参数变化(多入口/多终点/含岔口/深度域扩面)呈现面自适应义务(06 条目连带面②)——本卡不改呈现代码 | 反哺点 · 列清单(④呈现面随动需求登记归卡④/level-map 文档批) | -| `待办清单.md`(06 登记处) | 本卡登记条状态推进「已写卡(`reports/需求卡片-节点图生成器核心批-2026-10-21.md`)」(req-card 06 状态回写义务) | 反哺点 · 列清单(⑤状态回写与条目退役记载——本会话文件面限于本卡一个文件,回写由批次收口/用户发令执行,防四卡并行互踩) | -| `roadmap.md` | 迭代行编入笔与批内状态刷新 | 反哺点 · 列清单(⑥) | -| `game/core/node_map_generator.gd`、`game/data/schemas/level_gen_params.gd`、`game/data/tables/level_gen_params.tres`、`game/core/rng_service.gd`、`game/core/config_db.gd` | 生成器本体+参数档数据表+RNG 纯流面+ConfigDB 注册 | 实施本体(可写) | -| `game/tests/unit/core/node_map_generator_test.gd`、`game/tests/unit/core/node_map_growth_test.gd`、`game/tests/unit/data_config/level_gen_params_test.gd` | 验收断言面(§8 V 清单;game/smoke/ 已退役〔16c6f136〕——批 B 后断言面全落 `game/tests/` 宿主) | 实施本体(可写) | -| `game/tests/run_all.gd` | 三套新测试套件 manifest 登记(`_suites()` 追加三行 `expect=0`——manifest 数即闸 02 计数口径,未登记=闸 02 不执行) | 实施本体(可写) | - -## 7. 任务卡 - -> 估时按 Claude 工作日口径;每卡出口自跑轻闸;开工日 T0 复核本卡全部行号与取用条款行号。回归措辞模板沿用:里程碑步(T 段)出口跑一次全套闸;波内修复只做出红最小单元的最窄确认;新增测试先行孤立验证再并入全套。生成算法长图步骤(HOW)归实现(D1:28),本卡只钉产出规格、签名、取值与断言。 - -### 卡前 · 文件结构图 - -| 文件 | 职责 | -|---|---| -| `game/data/schemas/level_gen_params.gd`(新建) | 参数档 schema:五旋钮+战斗保底+岔口去路数域,`rows: Array[Dictionary]`(键=level_index)+`validate()` 域校验+`row_by_level_index()` 单点读 | -| `game/data/tables/level_gen_params.tres`(新建) | L1/L2/L3 三行参数档数据(provisional 值) | -| `game/core/node_map_generator.gd`(新建) | 生成器核心:`generate(params, seed) -> Dictionary` 产合法图;产出侧自证(产出约束五条+结构铁律+摆放约束三条);确定性(RngService 纯流面);参数无解 fail-fast | -| `game/core/rng_service.gd`(改) | 新增纯流面 `pure_stream(seed_val: int, scope: int) -> RandomNumberGenerator`(seed 键值、纯函数、不推 `_counter`、不落日志)——生成器结构取数唯一源 | -| `game/core/config_db.gd`(改) | 注册 `level_gen_params` 表(_tables 映射+成员+装载)+访问器 `level_gen_params_row(level_index: int) -> Dictionary` | -| `game/tests/unit/core/node_map_generator_test.gd`(新建) | 生成器验收断言(产出合格/可复现/岔路取舍/必经恰一店/归属锁定/多终点深度互异/战斗保底/L1 出口恒 1·大岔口 L2 起) | -| `game/tests/unit/data_config/level_gen_params_test.gd`(新建) | 参数档 schema validate+值域断言(五旋钮/合计 1/出口范围/层深下限) | -| `game/tests/unit/core/node_map_growth_test.gd`(新建) | 生长递增断言(L1/L2/L3 最长路径步数与节点总数递增、Δ(L2→L3)>Δ(L1→L2))+参数无解 fail-fast 断言 | - -### 接口块(全卡共用,邻卡引用以此为准——卡② T0 按此对齐) - -- **产出(供卡② 消费侧):生成器产图接口(本卡定稿)**——`generate(params: Dictionary, seed: int) -> Dictionary`。返回形状:`{"nodes": Array[Dictionary], "entries": Array[int], "terminals": Array[int]}`。节点字段:`{"id": int(图内唯一句柄), "type": String(battle/boss/reward/rest/shop), "depth": int(自入口最长路径步数), "next": Array[int](出边目标句柄)}`。`entries`=入口句柄数组(无入边节点,个数=params.entry_count);`terminals`=终点句柄数组(type=boss、无出边,个数=params.exit_count)。**两条硬语义不随签名浮动**:① 同(seed, params)⇒ 逐位同图;② 纯函数复现——图=(seed, params) 的函数,复现不依赖统一流全局计数器号位。**签名修正(对卡② 期望签名 `generate(level_index, params, seed)` 的定稿裁剪)**:`generate` **不收 `level_index`**(契约 6「生成器只吃参数档、不认关卡号」,D1:72);调用方自行 level_index→params 解析后调 `generate(params, seed)`。卡② T0 按本卡去掉 level_index 形参接线。 -- **产出(供卡②/卡③ 定位信息)**:节点 `depth`=遭遇坐标深度轴,喂 enc `node_depth`(键名不变、值=`depth`);多终点落在互异 `depth`(契约 2)。enc 三键 `level_index`/`node_depth`/`node_type` 与卡③ `resolve_enemy_rowset(L, D, row_kind)` 衔接零改动。 -- **产出(参数档数据表)**:`level_gen_params` schema+table;ConfigDB 访问器 `level_gen_params_row(level_index: int) -> Dictionary`(返回该关五旋钮行);卡② 只读该参数档传给 `generate`,不自定值。字段名(schema 键):`layers`(层深)、`branch_density`(分支密度)、`battle_ratio`/`reward_ratio`/`rest_ratio`/`shop_ratio`(配比四类)、`entry_count`(入口个数)、`exit_count`(出口个数)、`battle_floor`(战斗保底数)、`fork_ways_min`/`fork_ways_max`(岔口去路数,锁 2/3)。 -- **取用(RngService 纯流面)**:`RngService.pure_stream(seed_val: int, scope: int) -> RandomNumberGenerator`(本卡新增;生成器结构取数唯一源,禁 `open_stream`/`randomize()`/自建 RNG)。`scope`=生成器取数分域(代拍板 §10 ①:单流多次取数 vs 逐决策 scope,约束=纯函数、counter 无关、同图)。 -- **校验边界**:生成器产出侧自证(本卡)=产前强制过产出约束五条+结构铁律+摆放约束三条;卡② 消费侧加载校验(`validate_graph`)=独立防御(四检+良构),两者同源 D1 产出规格、分工不重叠(本卡不实现消费侧校验)。 - -### T0 事实坐实与跨卡接口对齐(≈0.5–1d) - -- [ ] 复核 §3 全部 file:line 与 §2 取用条款行号(D1 v1.1 条款行、F1–F8 落点),漂移即订正本卡。可核查结果:订正记录或「零漂移」声明进实现报告素材。 -- [ ] 与卡② T0 对齐四件:产图签名(去 level_index 的 `generate(params, seed)`)、图数据形状(id/type/depth/next 字段名)、参数档 schema 字段名、复现语义(纯函数/counter 无关两条硬语义确认);与卡③ 对齐 `depth`=遭遇坐标深度轴+多终点深度互异。可核查结果:对齐纪要(定稿签名对照 §7 接口块,漂移处回写邻卡接口块或记「一致」)。 -- [ ] 定稿 RNG 纯流面机制(§10 ① 域内):`pure_stream(seed_val, scope)` 具体签名与 scope 分域策略(约束=纯函数/counter 无关/同图/禁自建 RNG)。可核查结果:机制定稿进实现报告(签名+scope 取值)。 -- [ ] 定稿「支路」度量口径(§10 ② 域内):岔口各出边支路的分解(自出边起至再汇合/终点止的节点子图)与类型构成度量(集合 vs 多重集)。可核查结果:度量口径定稿进实现报告。 -- [ ] 改前读数基线采集(冒烟/单测通过数,两入口铁规下随 T1 迭代 loop 首跑取值)。可核查结果:基线读数记录在案。 -- [ ] §6 反哺点①–⑥ 条目起草(一节一改一依据)。可核查结果:反哺点草稿段落在 T4 产物成文前就绪。 - -### T1 参数档数据表(schema+.tres+ConfigDB 注册)(≈1–1.5d) - -- [ ] 实装 `game/data/schemas/level_gen_params.gd`:类 `LevelGenParams`(extends Resource),`rows: Array[Dictionary]`(键=level_index),`validate() -> Array`(域校验:五旋钮在域、配比四类合计 1、出口 L1=1/L2L3∈[1,3]、层深 L1≥6、岔口去路数 2/3、战斗保底≥0),`row_by_level_index(level_index: int) -> Dictionary` 单点读。可核查结果:schema 编译通过、validate 对合法行零错误对越域行报错。 -- [ ] 实装 `game/data/tables/level_gen_params.tres`:L1/L2/L3 三行五旋钮 provisional 值(取值照 §8.1 表逐行填)。可核查结果:表加载零诊断、三行值与 §8.1 provisional 表对上、validate 全绿。 -- [ ] 实装 `game/core/config_db.gd` 注册:`_tables` 映射加 `"level_gen_params": "level_gen_params.tres"`(对齐 :20 体例)、成员 `var level_gen_params`、装载(对齐 :142)、访问器 `level_gen_params_row(level_index: int) -> Dictionary`(对齐 :245 体例,缺行返回 `{}`)。可核查结果:ConfigDB 装载该表、访问器按 level_index 读回正确行、缺行返回空字典。 -- [ ] manifest 登记:`game/tests/run_all.gd` `_suites()` 追加 `{"path": "res://tests/unit/data_config/level_gen_params_test.gd", "expect": 0}`(对齐既有行体例;批 B 协议——禁「文件在 manifest 不在」空窗)。可核查结果:run_all 实跑套件数 +1 且新套件 PASS。 -- [ ] 验收步骤:跑 `godot --headless --path game --script res://tests/unit/data_config/level_gen_params_test.gd`,期望 schema validate+值域断言全绿。 -- [ ] 变异验证(注入→最窄闸见红→`git checkout` 还原→记录):把 L1 出口改 2(应=1)→ 期望值域断言红;把配比四类改合计 0.5 → 期望合计 1 断言红。记录表进实现报告。 - -### T2 生成器核心(产合法图+产出自证+RNG 纯流面确定性)(≈2.5–3.5d) - -- [ ] 实装 `game/core/rng_service.gd` 纯流面 `pure_stream(seed_val: int, scope: int) -> RandomNumberGenerator`:seed 键值纯派生(复用 `_mix64`/`derive_at` 派生模型)、返回 RNG、**不推 `_counter`、不落事件日志**;类头纪律注释补「纯流面=结构生成专用(生成器),counter 无关、同 (seed,scope) 同流」。可核查结果:同 (seed_val, scope) 两次 `pure_stream` 返回同序列;`counter()` 前后不变;不同 seed 序列不同。 -- [ ] 实装 `game/core/node_map_generator.gd`:类 `NodeMapGenerator`(extends RefCounted,Core 零 UI、RngService 经构造注入〔`--script` 禁裸 autoload,code-conventions:71〕);`generate(params: Dictionary, seed: int) -> Dictionary` 产出 `{nodes, entries, terminals}`(节点字段 id/type/depth/next)。**长图算法(HOW)归实现**(D1:28),只钉产出必须满足 §4 契约 3 全部产出约束+结构铁律+摆放约束三条+契约 2 深度赋值。取数只经 `pure_stream`(契约 1)。可核查结果:对 L1/L2/L3 参数档各以固定种子产图,图非空、字段齐备。 -- [ ] 实装产出侧自证 `validate_output(graph: Dictionary) -> Array`(生成器内、产前强制):过产出约束五条+结构铁律+摆放约束三条(道路系统/宽度/层高/归属锁定/战斗保底/无环/多入口多终点/每路径恰一入一出/商店位/岔路取舍两条/多终点深度互异)。可核查结果:合法产出自证零错误;构造违规夹具图(环/断头/孤儿/双终点同深/缺店/岔口同类型下一跳)各报对应错误类别(沿 `structural`/`dangling_next`/`dup_key`/`enum`/`domain` 词表)。 -- [ ] 实装确定性:`generate` 同 (seed, params) 两次逐位同图(深比较 nodes/entries/terminals);counter 无关(不同 `_counter` 初值下同 (seed,params) 同图)。可核查结果:`node_map_generator_test` 可复现断言绿。 -- [ ] manifest 登记:`game/tests/run_all.gd` `_suites()` 追加 `{"path": "res://tests/unit/core/node_map_generator_test.gd", "expect": 0}`(对齐既有行体例)。可核查结果:run_all 实跑套件数 +1 且新套件 PASS。 -- [ ] 验收步骤:跑 `godot --headless --path game --script res://tests/unit/core/node_map_generator_test.gd`,期望产出合格+可复现断言全绿。 -- [ ] 变异验证:生成器取数改 `open_stream`(推计数器)→ 期望 counter 无关断言红;把 depth 赋值改「最短路径」→ 期望「出边目标 depth>源」无环判据对某合流图误判/多终点深度互异断言红;产出自证注释「终点恒 Boss」判据 → 期望非 Boss 终点夹具红。记录表进实现报告。 - -### T3 验收断言批总装(生长递增/专项/参数无解 fail-fast)+变异验证(≈2–2.5d) - -- [ ] 实装 `game/tests/unit/core/node_map_generator_test.gd` 专项断言:① 含岔口与出口结构(三关参数档生成图各含 ≥1 岔口节点〔出度≥2,第一关即含、非单链取值〕;L1 出口恒 1、大岔口〔多终点归属锁定〕自 L2 起——L2/L3 exit>1 时图含分区域大岔口);② 岔路取舍·下一跳(任意岔口各去路下一节点类型互不相同);③ 岔路取舍·支路构成(任意岔口各支路类型构成互不相同、每支路至少一个独有类型);④ 必经恰一店(任意入口→终点路径恰过一家店);⑤ 归属锁定(多出口图每终点前一层去路集合恰含一个终点、各区域互不连达);⑥ 多终点深度互异(L2/L3 多终点 depth 两两不同);⑦ 战斗保底(战斗节点数 ≥ battle_floor)。可核查结果:七类断言对 L1/L2/L3 参数档×固定种子扫描面全绿。 -- [ ] 实装 `game/tests/unit/core/node_map_growth_test.gd` 生长递增断言:同子下 L1/L2/L3 生成图的最长路径步数与节点总数均逐关递增,且节点总数增幅 Δ(L2→L3)>Δ(L1→L2)。可核查结果:三档读数表断言绿。 -- [ ] 实装参数无解 fail-fast 断言:构造无解参数档(如 layers=1 且 branch_density 强制零岔口而出口要求分区域、或必经店无法满足)→ 期望 `generate` 致命中止(push_error+assert/既有 fatal,返回前不产出残图),无兜底图回退。可核查结果:无解参数夹具触发致命、断言「无残图返回、无兜底图路径」。 -- [ ] 变异验证(新增/改写断言逐条注入实验,记录表进实现报告):终点前驱改非店 → 期望「必经恰一店」红;把两终点设同 depth → 期望多终点深度互异红;把岔口两去路设同类型 → 期望岔路取舍·下一跳红;把战斗节点砍到低于 battle_floor → 期望战斗保底红;生长断言把 L2/L3 参数调成节点数增幅倒挂 → 期望生长递增红;把 `generate` 无解路径改「返回空图」→ 期望 fail-fast 断言红。逐条执行「注入→最窄闸见红→`git checkout` 精确还原→记录」。 -- [ ] manifest 登记:`game/tests/run_all.gd` `_suites()` 追加 `{"path": "res://tests/unit/core/node_map_growth_test.gd", "expect": 0}`(对齐既有行体例)。可核查结果:run_all 实跑套件数 +1 且新套件 PASS。 -- [ ] 验收步骤:T 段出口跑一次全套闸(`tools/run_all_gates.sh` 按改动域选档,本批碰 `game/` 代码与数据表+测试 → 代码域档),期望生成器/参数档断言全绿、探针段无红。 -- [ ] 件核:全仓 `generate` 调用点不传 level_index(契约 6);生成器内零 `randomize()`/零自建 RandomNumberGenerator/零 `open_stream`(契约 1,`pure_stream` 除外)。 - -### T4 产出物落位卡(≈0.5d,末卡——三份 `reports/` 文件写齐才算收口) - -- [ ] 轻闸全绿后、合并前写齐三份:`reports/iter-实现报告-<完成日>.md`(各卡状态+变异验证记录表+实机目检总清单+遗留读数义务)/`reports/iter-代拍板追认单-<完成日>.md`(§10 白名单内代拍项留痕:纯流面 scope 分域、支路度量口径、图内部持有结构等)/`reports/iter-反哺点-<完成日>.md`(§6 六条每条一落点:①生成规格补多终点深度互异+深度赋值句、②地图深度赋值消歧、③覆盖核对登记、④呈现面随动登记、⑤06 状态回写、⑥roadmap 编入笔)。可核查结果:三文件在 `reports/` 且 §6 同步面每条有落点。 - -**估时合计 ≈6.5–9d**(T0 0.5–1+T1 1–1.5+T2 2.5–3.5+T3 2–2.5+T4 0.5)。切分预案:T1 可单独一轮(数据层、不碰生成器);T2 为生成器主体(可再切「图数据结构+基本长图」/「约束自证+确定性」两小段,若一轮超 3.5d);T3 依赖 T2 产出(断言驱动生成器);超软盒(>9d)预挂:T2 若约束自证超一孔,先落产出约束五条+结构铁律+确定性主链,摆放约束三条+专项断言次轮补,全绿才收口。 - -## 8. 验证闸(逐需求点验收清单) - -| # | 需求点 | 测什么(断言/行为) | 落点/判定手段 | -|---|---|---|---| -| V1 | 产出合格(产出约束五条+结构铁律) | 任意参数档产出图过:道路系统(除岔口外每节点恰一条去路、岔口去路 2–3)/宽度分叉生长(宽度源于岔口、不摆平行层)/层高(L1≥6)/出口归属锁定/战斗保底;结构铁律(无环、多入口多终点且入口数/终点数=params.entry_count/exit_count、每路径恰一入一出);入口=无入边节点、终点=type=boss 且无出边 | 单测 `node_map_generator_test.gd`(产出自证零错误+入口/终点计数断言+违规夹具各报对应错) | -| V2 | 参数档 schema+.tres | 五旋钮在域、配比合计 1、出口 L1=1/L2L3∈[1,3]、层深 L1≥6、岔口 2/3、战斗保底≥0;L1/L2/L3 三行 provisional 值与 §8.1 对上;ConfigDB 单点读回正确 | 单测 `level_gen_params_test.gd`(validate+值域+访问器读回) | -| V3 | 可复现(纯函数+counter 无关) | 同(seed, params)两次产图逐位同图;不同 `_counter` 初值同图;同 params 不同 seed 不同图 | 单测 `node_map_generator_test.gd`(深比较+counter 无关) | -| V4 | 执行边界(无兜底图/fail-fast) | 参数无解 → 致命中止(push_error/assert),无残图返回、无兜底图/软回退路径;不合格产出不进玩法(自证不过不上图) | 单测(无解参数夹具触发致命)+件核(生成器零回退分支) | -| V5 | 岔路取舍两条 | 任意岔口各去路下一节点类型互不相同;各支路节点类型构成互不相同、每支路至少一个独有类型 | 单测 `node_map_generator_test.gd`(岔口扫描) | -| V6 | 必经恰一店+归属锁定+多终点深度互异 | 任意入口→终点路径恰过一家店;多出口图每终点前一层去路集合恰含一个终点、各区域互不连达;多终点 depth 两两不同 | 单测 `node_map_generator_test.gd`(DFS 路径枚举+区域可达+depth 比对) | -| V7 | 战斗保底 | 任意合法产出战斗节点数 ≥ battle_floor | 单测 `node_map_generator_test.gd`(计数断言) | -| V8 | 生长递增 | L1/L2/L3 最长路径步数与节点总数逐关递增;Δ(L2→L3)>Δ(L1→L2) | 单测 `node_map_growth_test.gd`(三档读数对拍) | -| V9 | 含岔口与出口结构(第一关即含岔口/L1 出口恒 1/大岔口自 L2 起) | 三关参数档生成图各含 ≥1 岔口节点(出度 ≥2,第一关即含、非单链退化取值);L1 生成图终点恰 1(无多终点归属大岔口);L2/L3 exit>1 时含分区域大岔口(归属锁定) | 单测 `node_map_generator_test.gd`(岔口存在性+出口计数+区域结构) | -| V10 | RNG 单点(纯流面) | 生成器取数只经 `pure_stream`;`counter()` 前后不变;零 `randomize()`/零自建 RNG/零 `open_stream` | 件核+单测(counter 不变断言)+静态扫描 | -| V11 | 反哺点清单 | §6 六条每条有落点 | 件核对表(T4) | - -### 8.1 参数档 provisional 取值表(provisional 占位,定值批落终值;数值全 provisional) - -> 域校验(V2)对全部行强制:配比四类合计=1;出口 L1 恒 1、L2/L3∈[1,3];层深 L1≥6 且逐关递增增幅变大;岔口去路数恒 (2,3);战斗保底≥0。取值须使 V8 生长递增成立(L1/L2/L3 最长路径步数与节点总数递增、Δ(L2→L3)>Δ(L1→L2));provisional 可在域内微调(§10 ④),禁改已锁域(层深下限、出口 L1=1、岔口 2/3、商店数=出口数)。配比 `shop_ratio` 为名义值——商店实数由摆放约束①钉死=出口数(每终点前一店),生成器以「摆放优先、配比填余、战斗保底兜底」次序兑现。 - -| 参数(schema 键) | L1 | L2 | L3 | 域/状态 | -|---|---|---|---|---| -| `layers`(层深=最长路径步数) | 6 | 9 | 13 | ≥6,逐关递增增幅变大(L1 下限已锁,L2/L3 provisional) | -| `branch_density`(分支密度) | 0.30 | 0.50 | 0.70 | [0,1],provisional;逐关递增(决策点密度递增);L1 取值须保证第一关即含 ≥1 岔口 | -| `battle_ratio`/`reward_ratio`/`rest_ratio`/`shop_ratio`(配比) | .50/.20/.20/.10 | .50/.20/.15/.15 | .55/.15/.15/.15 | 四类合计 1,战斗保底优先;provisional | -| `entry_count`(入口个数) | 1 | 2 | 2 | ≥1,provisional | -| `exit_count`(出口个数) | 1 | 2 | 3 | L1 恒 1(已锁);L2/L3∈[1,3](已锁范围),provisional | -| `battle_floor`(战斗保底数) | 4 | 6 | 8 | ≥0,provisional | -| `fork_ways_min`/`fork_ways_max`(岔口去路数) | 2/3 | 2/3 | 2/3 | 恒 2/3(已锁) | - -**测试方式两分法(2026-10-11 用户定)**:V1–V11 全部机检自动化(单测为主);能力验证可经调试接口直改参数档/种子驱动生成图(沿用现役 DebugOverlay/DS_DEBUG 门控,不走正常资源积累流程)。**用户目检项**:「三关生成图选路观感(多入口/大岔口取舍/多终点走向可见、第一关即有选择)」+「生成图规模随关卡推进变大(生长递增可感知)」两条——观测项写进 `reports/iter-实现报告-<完成日>.md` 实机目检总清单,登记不阻塞(验收时间用户自行安排;呈现依赖卡②/卡④ 落地后目检)。 - -**闸口径**:轻闸每任务卡出口自跑;批末重闸=`tools/run_all_gates.sh` 按改动域选档(本批碰 `game/` 代码与数据表+测试 → 代码域档)全绿才合并+改前改后读数对拍。回归措辞:里程碑步(T 段)出口跑一次全套闸;波内修复只做出红最小单元的最窄确认;新增测试先行孤立验证再并入全套。 - -**变异验证伴生**(新增/改写验收函数逐条注入实验,记录表进实现报告):L1 出口改 2(V2 红)/配比合计 0.5(V2 红)/取数改 `open_stream` 推计数器(V3 counter 无关红)/depth 赋值改最短路径(V3/V6 多终点深度互异或无环红)/产出自证注「终点恒 Boss」(V1 红)/终点前驱改非店(V6 红)/两终点同 depth(V6 红)/岔口两去路同类型(V5 红)/战斗砍低于 battle_floor(V7 红)/生长倒挂(V8 红)/无解路径改返回空图(V4 红)/生成路径强制无岔口(V2/fv9 岔口存在性红)/L1 参数出口置 2 产出侧(fv9 出口计数红)。变异不红则改判据或如实标注拦截边界。 - -### 8.2 改动—验收对照表(2026-10-21 验收闭合批新规,test-conventions §9.3) - -> 各新增断言 desc 统一尾缀〔fv#〕(本表「验收测试」列可 grep 定位);撤销/变异证明逐条对应 §8 变异验证伴生与 T1–T3 变异步、T4 件核(每张任务卡至少一条,test-conventions §9.4)——T0 为事实对齐卡(无行为改动),其事实核对红证并入 fv2(参数档 schema 事实面)与 fv10(RngService 现用法事实面)。层级列只填 §9.2 四层词(单元/接口/集成/探针);件核是证明方式不是层级,留在「撤销或变异证明」格。纯产出物行按 §9.1 标「非行为改动」。 - -| 需求点 | 改动(文件:行/行为) | 验收测试(名称+文件:行+尾缀) | 层级 | 真实入口 | 撤销或变异证明 | -|---|---|---|---|---|---| -| fv1 产出合格(产出约束五条+结构铁律) | `game/core/node_map_generator.gd`(新建)`validate_output()` 产出侧自证:道路系统/宽度/层高/归属锁定/战斗保底+结构铁律(无环/多入口多终点/每路径恰一入一出) | `node_map_generator_test` 产出合格+违规夹具〔fv1〕 `game/tests/unit/core/node_map_generator_test.gd` | 单元 | `NodeMapGenerator.validate_output(graph)`+`generate(params, seed)` 产前强制 | 变异:产出自证注「终点恒 Boss」→ 非 Boss 终点夹具红(T2) | -| fv2 参数档 schema+.tres | `game/data/schemas/level_gen_params.gd`(新建)`validate()`/`row_by_level_index()`;`game/data/tables/level_gen_params.tres`(新建)三行;`game/core/config_db.gd` 注册+访问器 `level_gen_params_row()`(对齐 :20/:142/:245 体例) | `level_gen_params_test` validate+值域+读回〔fv2〕 `game/tests/unit/data_config/level_gen_params_test.gd` | 单元 | `LevelGenParams.validate()`/`ConfigDB.level_gen_params_row()` 直调 | 变异:L1 出口改 2/配比合计 0.5 → 值域、合计 1 断言红(T1) | -| fv3 可复现(纯函数+counter 无关) | `node_map_generator.gd` 同(seed, params)逐位同图+counter 无关;`game/core/rng_service.gd` `pure_stream()` 不推 `_counter` | `node_map_generator_test` 可复现(深比较+counter 无关)〔fv3〕 `game/tests/unit/core/node_map_generator_test.gd` | 单元 | `generate(params, seed)` 同种子两次直调 | 变异:取数改 `open_stream` 推计数器 → counter 无关断言红(T2) | -| fv4 执行边界(无兜底图/fail-fast) | `node_map_generator.gd` 参数无解致命中止(push_error/assert),无残图、无兜底回退分支 | `node_map_growth_test` 参数无解 fail-fast〔fv4〕 `game/tests/unit/core/node_map_growth_test.gd`+件核(生成器零回退分支) | 单元 | 无解参数档调 `generate(params, seed)`(期望 fatal) | 变异:无解路径改「返回空图」→ fail-fast 断言红(T3) | -| fv5 岔路取舍两条 | `node_map_generator.gd` `validate_output()` 岔路取舍判据(下一跳类型互异/支路构成互异) | `node_map_generator_test` 岔路取舍两条〔fv5〕 `game/tests/unit/core/node_map_generator_test.gd` | 单元 | `validate_output()` 岔口扫描直调 | 变异:岔口两去路设同类型 → 下一跳断言红(T3) | -| fv6 必经恰一店+归属锁定+多终点深度互异 | `node_map_generator.gd` 商店位/归属锁定/多终点 depth 互异判据(契约 2/5) | `node_map_generator_test` 必经恰一店+归属锁定+多终点深度互异〔fv6〕 `game/tests/unit/core/node_map_generator_test.gd` | 单元 | `validate_output()`+DFS 路径枚举/区域可达/depth 比对 | 变异:终点前驱改非店→必经恰一店红、两终点同 depth→深度互异红(T3);depth 赋值改最短路径→互异或无环红(T2) | -| fv7 战斗保底 | `node_map_generator.gd` 战斗保底判据(battle_floor) | `node_map_generator_test` 战斗保底〔fv7〕 `game/tests/unit/core/node_map_generator_test.gd` | 单元 | 产图战斗节点计数断言 | 变异:战斗节点砍低于 battle_floor → 战斗保底红(T3) | -| fv8 生长递增 | 三档参数档产图读数(`level_gen_params.tres` provisional 域内取值) | `node_map_growth_test` 生长递增三档读数〔fv8〕 `game/tests/unit/core/node_map_growth_test.gd` | 单元 | L1/L2/L3 `generate` 读数对拍 | 变异:L2/L3 参数节点数增幅倒挂 → 生长递增红(T3) | -| fv9 含岔口与出口结构(第一关即含岔口/L1 出口恒 1/大岔口自 L2 起) | `node_map_generator.gd` 道路系统/出口归属产出(契约 3) | `node_map_generator_test` 岔口存在性+出口计数+区域结构〔fv9〕 `game/tests/unit/core/node_map_generator_test.gd` | 单元 | 产图结构扫描(三关参数档×固定种子) | 变异:生成路径强制无岔口(分支密度 0)→ 岔口存在性断言红;L1 参数出口置 2 → 出口计数断言红(T1/T3 变异步,与 fv2 出口值域注入同发) | -| fv10 RNG 单点(纯流面) | `game/core/rng_service.gd`(改)新增 `pure_stream(seed_val, scope)` 纯流面(不推 `_counter`、不落日志),生成器取数唯一源 | `node_map_generator_test` counter 不变断言〔fv10〕 `game/tests/unit/core/node_map_generator_test.gd`+件核/静态扫描(零 `randomize()`/零自建 RNG/零 `open_stream`) | 单元 | `RngService.pure_stream()` 直调+`counter()` 前后读数 | 变异:取数改 `open_stream` 推计数器 → counter 不变断言红(T2,与 fv3 同发注入) | -| fv11 反哺点清单(**非行为改动**——纯文档产出物,无游戏行为,§9.1) | §6 六条反哺点落 `reports/iter-反哺点-<完成日>.md`(T4) | 件核对表〔fv11〕(§6 六条每条一落点)`reports/iter-反哺点-<完成日>.md` | 非行为改动 | T4 产出物件核(六条落点逐条核对) | 撤销:反哺点缺条 → 件核对表不收口(T4,纯文档产物无代码变异) | - -**旧断言预声明(test-conventions §9.5)**:本卡零旧断言改写——断言面全新建(node_map_generator/growth/level_gen_params 三套新测试),`rng_service.pure_stream()` 为新增纯函数、不触既有断言。实现期确需改旧断言=上游缺陷,停卡呈报;未声明的削弱由独立审计判红。 - -**独立验收审计(test-conventions §9.7,2026-10-21 起)**:合并前由未参与实现的迭代 loop 内子代理(或用户点名会话)审计——只读本卡、实现报告实际对照表、diff 与测试文件,自跑最窄测试抽查,不采信「已全绿」;审计报告落 `reports/iter-验收审计-<完成日>.md`(逐行「通过/缺漏/例外及依据」)。**审计未通过不得合并**(T4 合并前置)。 - -## 9. 异常分级 - -- **L1(自救)**:断言措辞、命名、注释、错误类别命名沿用;参数档字段注释与 provisional 标注。 -- **L2(记录继续)**:RNG 纯流面机制细节(`pure_stream` 签名形态、scope 分域策略);「支路」度量口径(分解边界、集合 vs 多重集);生成图在生成器内的持有结构;长图算法具体步骤(D1:28 归实现);参数档 .tres 行键/字段名细节(§10 域内)。 -- **L3(停机呈报)**:纯流面机制与「随机只经 RngService 统一流」红线不可调和(若「统一流」被裁定仅指 counter 流、纯流面不算——须用户改契约 1);执行中发现同 depth 多终点图样(推论条款 A 被破,契约 2);任何触碰「生成器只吃参数档不认关卡号」「纯函数复现」「无兜底图/fail-fast」的诉求;生成图深度域超出敌/板表覆盖且数据批无人接手(§0 交付序列失管);产出约束/结构铁律/摆放约束被判据要求放宽(违 D1:41-51)。 - -## 10. 代拍板白名单(超清单必呈报) - -1. RNG 纯流面机制细节(`pure_stream(seed_val, scope)` 具体签名、`scope` 分域策略=单流多次取数 vs 逐决策 scope、派生式复用 `_mix64`/`derive_at` 的哪种)——约束:纯函数、counter 无关(不推 `_counter`)、同(seed, params)同图、随机只经 RngService、禁自建 RNG/`randomize()`/`open_stream`(契约 1)。 -2. 「支路」度量口径(岔口各出边支路分解=自出边起至再汇合/终点止;类型构成度量=集合或多重集)——约束:满足「各支路构成互不相同、每支路至少一个独有类型」设计意图(D1:84),可被 V5 断言核查。 -3. 生成图在生成器内的持有结构、节点句柄载体(int id 的赋值序)、`generate` 返回字典的内部组装——约束:读面字段名 id/type/depth/next 不变(§7 接口块)、句柄唯一、depth=自入口最长路径步数。 -4. 参数档 .tres 行键/字段名内部细节(键=level_index 的行式 vs 其它)、provisional 取值微调(保持 §8 域与生长递增成立)——约束:五旋钮齐全、域校验过、生成器只读不改值、数值全 provisional(定值批落终值)。 -5. 长图算法具体步骤(怎么从旋钮长出一张合法图)——约束:产出必过 §4 契约 3 全部+确定性(D1:28「图怎么长出来是实现的事」)。 -6. 提交切分与 worktree 分支命名。 - -## 11. 待裁点 - -**零待裁点,开工门=U0。** - -五处灰色地带已按「设计权威文档+硬纪律+跨卡派发」推导定值(用户可否决,随 T4 反哺点清单呈报可见): - -- **RNG/复现机制(契约 1,卡② defer 给本卡)**:备选「`open_stream` 统一流取数」——淘汰依据=`open_stream` 推全局计数器(`rng_service.gd:86-99`),产图随号位漂移,与「同种子+同参数档=同图」(D1:60)+「断点停点号位重生成同图」(D3:264 存档复现)冲突(resume 在停点号位、非产图号位);备选「自建 RandomNumberGenerator」——淘汰依据=AGENTS.md:73「禁自建 RandomNumberGenerator」红线;**定值=RngService 纯流面** `pure_stream(seed, scope)`(seed 键值、纯函数、不推 counter),生成器取数唯一源。依据=D1:60+D3:264+AGENTS.md:73 合取,counter 无关是「存档复现重生成」的必然要求;**「统一流」口径加固**:RngService 本就含纯派生面 `derive_at(scope)`(`rng_service.gd:34-37`,不推计数器)+号位取号面 `open_stream` 双面,纯流面即 `derive_at` 的流化(同服务内既有分工、非另起第二套流),故「随机只经 RngService 统一流」=经 RngService 服务取数成立(自建 RNG 才是红线违例)。**唯一残余风险**:若用户裁定「统一流」严格专指 counter 取号流、纯流面不算,则契约 1 须改(L3 呈报口,§9 已列)——本卡按「RngService 服务内既有纯派生/取号双面」定值。 -- **节点深度赋值(契约 2,卡② grilling 注记「depth 赋值一致性归卡①」)**:备选「最短路径(BFS 距离)」——淘汰依据=合流/多入口下出边目标 depth 可 ≤ 源(如目标经更短入口可达),「出边目标 depth 严格大于源」无环判据(卡② 契约 1④)失效;**定值=自入口最长路径步数**(多入口取距任一入口最长)。依据=D1:47「层高=沿路步数(最长路径)」+D2:65「节点深度=自入口步数」+无环判据有效性(最长路径使每边 target>source)三者合取。 -- **多终点深度互异(契约 2,卡③ 契约 6 推论条款 A 派发本卡兑现)**:备选「同 depth 多终点」——淘汰依据=「定位信息只有两样(关,深)」(D2:119)下同 depth 多终点不可区分,「多终点各配 Boss 行、走哪条路决定打谁」(D1:96)落空;**定值=多终点落在互异 depth**(图侧,本卡产出约束)。依据=D2:119+D1:96+卡③ 契约 6 合取;(深度带互异的带切法归卡③ 契约 5,本卡不读敌表)。 -- **「支路」度量口径(摆放约束③)**:备选「全子图类型多重集」——淘汰依据=合流支路共享尾段使多重集趋同、判据过弱;**定值=岔口各出边支路=自出边起至再汇合/终点止的节点子图,类型构成按该互斥段度量**(集合或多重集落 §10 ②)。依据=D1:84「各支路构成互不相同、每支路至少一个独有类型」的设计意图(每条路通向不同的东西)+可核查性。 -- **generate 签名去 level_index(契约 6,对卡② 期望签名的裁剪)**:备选「保留 `generate(level_index, params, seed)`」——淘汰依据=D1:72「生成器只吃参数档、不认关卡号」明文,收 level_index 即违反;**定值=`generate(params: seed)`(去 level_index 形参),调用方 level_index→params 解析后调用**。依据=D1:72 直读;卡② 契约 7「签名以卡①定稿为准」授权本卡裁剪,卡② T0 对齐接线。 - ---- - -**自审五查记录(2026-10-21)**:① 规格覆盖——§1 四件本体/八项完成定义逐条挂 T 卡(参数档=T1、生成器产出合格+确定性=T2、专项+生长+fail-fast+变异=T3、反哺点=T4;确定性横跨 T2 纯流面+V3 断言),无漏项;② 步骤扫描——全部步骤一步一动作一可核查结果,实现体只给签名+判据名(`generate`/`pure_stream`/`validate_output` 签名、产出约束判据清单、七类专项断言名),长图算法(HOW)明写归实现(D1:28)不抄写、无占位符、无推诿措辞、无本批未定义的类型/函数名;③ 类型一致性——§7 接口块签名与 T 卡/邻卡引用逐字一致(`generate(params: Dictionary, seed: int) -> Dictionary`、返回 `{nodes,entries,terminals}`、节点 `{id,type,depth,next}`、`pure_stream(seed_val: int, scope: int) -> RandomNumberGenerator`、`level_gen_params_row(level_index: int) -> Dictionary`、字段名 layers/branch_density/battle_ratio/reward_ratio/rest_ratio/shop_ratio/entry_count/exit_count/battle_floor/fork_ways_min/fork_ways_max);④ 审查焦点——补挂失败模式:产图失败回退残图(V4 变异)、复现漂移(V3 变异)、多终点同深错兑付(V6 变异)、岔口同类型下一跳(V5 变异)、必经店漏店(V6 变异)、战斗被挤没(V7 变异)、生长倒挂(V8 变异)、取数推计数器(V3 counter 无关变异);⑤ 篇幅比例——规格约 2 屏、任务卡步骤与实现面 1:1,函数体零展开(长图算法不抄写),产出规格/执行边界照录 D1 不增删。 - -**v1.1 修订自审(2026-10-24,宿主重锚批——六查复核)**:动笔面=§3 行号重锚(F2 探测块 :156-159→:149-158 等、F3 校验体随 level_table.gd 现态、F4 enc 组装 :467-482→:513-525、F7 validate :17-31→:13-27)+§6 断言面行 smoke_all→tests 三宿主+run_all manifest 行+T1/T2/T3 各补 manifest 登记步(阻断 4 闭合)+归属句卡③④ 状态换代(已落地·迭代 34)。①规格覆盖——设计面零变动(§1/§2/§4/§8 未触),新增 manifest 步各挂 T1/T2/T3 与 fv2/fv1-fv3/fv4-fv8 宿主执行面;②步骤扫描——三步均一步一动作+可核查结果(实跑套件数 +1);③类型一致性——签名与邻卡引用零变动;④改动覆盖——本修订纯指认面订正+测试基建登记步,零游戏行为改动,无新增对照表行义务;⑤篇幅比例——零函数体;⑥落位复用——manifest 登记沿批 B 既有行体例(`{"path":…, "expect": 0}`)、断言宿主沿批 B 16 宿主体制,无新增件无重复落点。重锚依据=`reports/批B-断言迁移账本-2026-10-22.md`+2026-10-24 master 实查(16c6f136 后现态)。 - -**grilling 复核记录(2026-10-21,设计树对抗复核,不采访用户——可推导定值写依据、真归用户列 §11)**:按设计树走查八支——① RNG/复现(三案淘汰两案定纯流面,§11 留统一流口径否决口);② 深度赋值(最长路径 vs 最短路径,最短路径破无环判据→定最长路径);③ 多终点深度互异(卡③ 推论条款 A 派发→定互异 depth,深度带归卡③);④ generate 签名(D1:72「不认关卡号」→去 level_index,卡② 授权裁剪);⑤ 支路度量(互斥段口径保设计意图+可核查);⑥ 产出约束五条 vs 结构铁律 vs 摆放约束的分工(五条=合法图定义、铁律=无环/多入多出/每路径恰一入一出、摆放=商店位/岔路取舍——三者产出自证全兜,卡② 加载校验是独立防御不重叠);⑦ 大岔口自 L2 起(D1:70「大岔口自第二关起、第一关出口恒 1」→L1 出口恒 1、多终点归属锁定 L2+,与「第一关即含岔口〔普通选择岔口〕」不矛盾——普通岔口 L1 有、归属大岔口 L1 无);⑧ 覆盖依赖(生成深度域 vs 敌/板表覆盖=真实跨卡缺口,卡③ §5 已登记归口后续表批——本卡写死交付序列依赖 §0,不扩敌数值)。code-conventions 对照:§2.2「位置/句柄/键语义单一」= 契约 2(id/depth 拆分)的规范依据(node_depth 双语义是现役违规态);§71 `--script` 禁裸 autoload=生成器 RngService 注入依据;§4.11 新单测不经 autoload 无冲突。AGENTS.md 硬红线逐条过:数值单点(参数档 .tres 经 ConfigDB、生成器零数值字面量定值)、RNG 单点(纯流面出自 RngService、禁自建 RNG/randomize——「统一流」口径留 §11 否决口)、测试两入口(T 卡只写断言不跑测)、实现批禁写 design/(§6 义务全走反哺点、零旧口径的同步措辞)、fail-fast(参数无解致命无兜底图)、数值全 provisional(定值批落终值)。**本轮对抗复核实改两处**:① 入口/终点数=参数档(D1:51「个数=参数档」)此前无验收断言兜住(入口/出口两旋钮兑现缺口)→ 补进 V1 结构铁律;② 纯流面「统一流」口径加固(援引 RngService 已有纯派生面 `derive_at`〔rng_service.gd:34〕,纯流面=其流化非第二套流,「只经 RngService 统一流」成立)。结论:零待裁点,五处灰色推导已定值并留 §11 供用户否决。 diff --git a/reports/需求卡片-重构boardview拆件批-2026-10-24.md b/reports/需求卡片-重构boardview拆件批-2026-10-24.md new file mode 100644 index 00000000..3ebfb978 --- /dev/null +++ b/reports/需求卡片-重构boardview拆件批-2026-10-24.md @@ -0,0 +1,155 @@ +# 需求卡片:重构 boardview 拆件批(瓦片件 TilePalette+英雄精灵件 HeroSprite 拆出) + +- 版本:**v1.2**(2026-10-24 v1.0 成文→2026-10-25 grilling 第一轮修订 v1.1〔三阻断+四应修折入〕→第二轮复核「通过(附应修)」,两条应修(查表族棋盘态依赖/TILE_ 常量直读归属)折入定稿;复核结论见 §12) +- 来源:[待办清单](../待办清单.md)「game 代码结构审查批——行为缺陷定点修+结构重构候选」条目之结构重构候选(2026-10-24 用户裁**五件全部立项**,本卡为 board_view 拆件件)(唯一事实源:工程直入证据指认=审计报告 [reports/审计-game代码结构审查-2026-10-24.md](审计-game代码结构审查-2026-10-24.md) §三 大文件表 board_view 行+本写卡会话结构实查〔本卡 §3——审计「瓦片算法 ~230 行」经实查须并入查表族与缓存态方成立,§12 订正回呈〕) +- 归属:`game/presentation/views/board_view.gd`(拆出源)+ `game/presentation/views/tile_palette.gd`(**新增**·瓦片件——静态算法+装载缓存+查表一体)+ `game/presentation/views/hero_sprite.gd`(**新增**·英雄精灵件,Node2D)+ 调用点改引 +- 排期:未编入(迭代批卡片;估时 ≈1.5 工作日);**前置=卡①先行落地**(其 d18 触点 :998 在瓦片族内);与 R1/R3 零文件交叠 +- 开工门:U0+grilling 复核门过(§12) + +## 0. 前置依赖与并行性 + +- **第〇步核对(2026-10-24 写卡会话)**:来源条目设计面=工程直入、状态=待写卡;重构候选 2026-10-24 用户裁全部立项——工程直入重构类,凭审计 §三行号级证据放行。 +- **顺序硬约束**:卡①先行(d18 lumas_out 类型补齐后随族迁移,等价搬运不回退);卡② S9 注解若先行,board_view 位点注解随迁。R1/R3 零文件交叠可并行波。 +- **与迭代 36(已实现入库)**:文案清扫批 wa8 触点 `board_view.gd:730/:778/:782`(字形与哨兵)与 iter36 wc5 的 `ui_contrast_probe.gd:150` 直引(grilling 实查仍在原址未漂)——本批直引改引后探针调用同步改址(B2 清单),零 hunk 交叠实测成立。 +- **gdstyle S13(已落地)**:两新件落在 S13 扫描面——基线零新增。 +- 棋盘 golden 基线由 `board_shot_probe`+auto_player novice 档生成(文案卡 F7 同源事实)——本批等价拆件,基线像素必须逐位不变。 + +## 1. 目标与完成定义 + +**目标**:board_view.gd(1219 行,五职责混装)的「最划算两刀」落地——①瓦片族(装载/翻转/明度归一/墨族模式/对比度/哈希查表,静态算法+装载缓存一体)拆出为 `tile_palette.gd`;②英雄精灵族(~103 行)拆出为 `hero_sprite.gd`(Node2D 件——`create_tween()` 为 Node 方法,硬约束)——board_view 收在 **≈880 行**(实码重算:净拆出 ≈341 行),退回「格面渲染+输入翻译+探针面」三职责;零行为变更,棋盘 golden 三图逐像素不变。 + +**改动单元清单**(🔴 契约 1): + +| # | 拆出块(实查行界与净行数) | 去向 | 依据 | +|---|---|---|---| +| r2-1 | 瓦片族全带:`_load_board_tiles`(:932,**持七成员装载缓存** :970-978:`_tiles`/`_floor_tiles`/`_tiles_board_type`/`_tiles_ok`/`_digit_dark_ink`/`_digit_plate`/`_seal_tex`——grilling 订正「无状态装载器」失实)/`_request_dungeon_fallback`(:980)/`_flip_tex`(:987)/`_normalize_floor_luma`(:998)/`_srgb_lin`(:1024)/`_digit_ink_mode`(:1040)/`_contrast_y`(:1069)/`_ink_y`(:1074,`_contrast_y` 唯一消费——随迁防跨件反调)/`_cell_hash`(:1081,查表族消费)+**查表族** `_is_wall`(:1089)/`_wall_tile_for`(:1098)/`_wall_center_tile`(:1136)/`_floor_tile_for`(:1155)——:932-1155 净 ≈238 行 | 新件 `tile_palette.gd`=**瓦片件**(装载缓存七成员随迁持于件内+六 static 算法+查表族经**棋盘态注入**——第二轮复核订正:`_is_wall`(:1089-1093) 读 `_board.width/height`+`_minesweeper.is_revealed(c)`、`_wall_tile_for` 四邻判定全经 `_is_wall`,件内拿不到棋盘态——查表族以件持 board/minesweeper 引用实现(rebuild 换板时随 `load_board_tiles` 同步更新),board_view 持 `tile_palette` 实例成员;v1.0「纯静态装配器」形态经 grilling 订正——装载器持缓存态,件形态定案为「持缓存装配器」) | 审计 §三「瓦片静态算法+哈希查表(~230 行)」——查表族并入方达该行数口径(§12 订正回呈) | +| r2-2 | 英雄精灵族:`_update_hero`(:387)/`_place_hero`(:410)/`_hero_frame_flash`(:434)/`notify_hero_move`(:451)/`play_hero_dig_bounce`(:457)/`_hero_feet`(:480)/`hero_hand_point`(:487)/`hero_sprite_scale`(:493)——净 ≈103 行(`cell_origin` :472-479 留驻) | 新件 `hero_sprite.gd`=**Node2D 件**(形态定案依据:`create_tween()` 是 Node 方法,英雄族三处调用——RefCounted 无此 API;grilling 订正 v1.0 两态待定);依赖面=grilling 实测清单:`_cell`/`SEP`/`_svp_container.position` 三几何量+`_world`+三纹理+状态量+tween 句柄(`_hero_feet` 自算坐标不调 `cell_origin`——B3) | 审计 §三「英雄精灵状态机(~110 行)」 | +| r2-3 | 调用点改引:board_view 内部消费点+外部直引三处(`terrain_tile_sets_test.gd:135` `_normalize_floor_luma`+`:139` `_digit_ink_mode`——grilling 补查、`ui_contrast_probe.gd:150` `_normalize_floor_luma`) | 直引改 `TilePalette.` 前缀同形调用(断言判据零变化);board_view 内部改经件成员/类前缀 | 探针/测试缝 | + +**完成定义**: + +1. 三单元落地;**行数读数记录**(board_view 预期 ≈880、两新件各自净行数)——读数项非停机闸(拆出量以函数清单为准); +2. **等价三证**齐:①逐字迁移核对表(六 static 逐字搬运;装载器/查表/英雄族除接收者化与缓存成员随迁外逐字一致);②全套闸绿+改前改后读数对拍;③**棋盘三图 golden 逐像素 changed==0**(瓦片归一/墨族/英雄渲染全在像素路径上——本卡最强等价判据)+菜单 golden 同判; +3. 两新件合规(S9 全类型/S10 头注/S11 零禁用词/S13 基线零新增); +4. 至少一条撤销证明记录(T2);三份产出物写齐(T3)。 + +## 2. 设计事实源 + +| # | 出处 | 取用条款 | +|---|---|---| +| D1 | 工程直入证据指认=审计 §三 大文件表 board_view 行(1219 行;「最划算两刀」指认)+本会话结构实查(§3——查表族与缓存态归属经 grilling 订正,~230 行口径以并入查表族成立) | 拆出块清单与行界 | +| D2 | [code-conventions](../design/engineering/code-conventions.md) §1 结构纪律 | 一文件一职责;迁移禁顺手改写 | +| D3 | [architecture](../design/engineering/architecture/architecture.md) 呈现层 D1 行 | BoardView=Control 壳+SubViewport+Node2D 世界的探针契约面不变 | +| D4 | 2026-10-24 用户 P0 令 | 行为验收只走撤销证明,禁注入造红 | + +## 3. 现状事实(2026-10-24 写卡会话实查+2026-10-25 grilling 实测;开工日 T0 复核行号) + +| # | 事实 | 出处 | +|---|---|---| +| B1 | board_view.gd 1219 行;瓦片族全带 :932-1155(六 static+装载器+fallback+`_ink_y`+`_cell_hash`+查表四函数);**装载器持七成员缓存** :970-978(`_tiles`/`_floor_tiles`/`_tiles_board_type`/`_tiles_ok`/`_digit_dark_ink`/`_digit_plate`/`_seal_tex`)——「实例无状态」陈述失实(grilling 订正) | `game/presentation/views/board_view.gd:932-1155` | +| B2 | 外部直引三处:`terrain_tile_sets_test.gd:135`(`BoardViewScript._normalize_floor_luma(tex, 0.10, lumas)`)+`:139`(`BoardViewScript._digit_ink_mode(lumas)`)+`ui_contrast_probe.gd:150`(`BoardView._normalize_floor_luma(tex, 0.10, null)`——iter36 wc5 后仍在原址);其余函数直引面 T0 复扫 | 上列 file:line | +| B3 | 英雄族依赖面(grilling 实测):`_hero_feet`(:480) 自算 `Vector2(c)*(_cell+SEP)` **不调 cell_origin**;实际耦合=`_cell`/`SEP`/`_svp_container.position`(`hero_hand_point` :487 读)三几何量+`_world`+三纹理+状态量+三 tween 句柄;**board_view 无 HeroState 枚举**(全文件 grep 零命中——英雄态=纹理切换+布尔+tween 句柄,v1.0「HeroState 迁移与否 T0 定」悬空点撤销) | `game/presentation/views/board_view.gd:387-497` | +| B4 | 瓦片族消费链:`_load_board_tiles`←`rebuild`(:258)/`set_desaturated`;`_digit_ink_mode`←`_paint_cell`(:708) 墨族模式;查表族←`_paint_cell` 装配路径(:1145/:1156 消费 `_cell_hash`)——全部在棋盘像素路径上(golden 铁证依据)。**查表族棋盘态依赖(第二轮复核)**:`_is_wall` 读 `_board` 尺寸+`_minesweeper.is_revealed`——件需棋盘引用注入且随换板更新;**TILE_ 常量族**(`TILE_WALL_NAMES`/`TILE_FLOOR_VARIANTS`/`TILE_FLOOR_FMT`,装载器 :936-937 注释自证被测试直读)外部直读五处:`terrain_tile_sets_test.gd:44/45/125`、`ui_contrast_probe.gd:146/147`——常量随件迁(瓦片域唯一宿主),五处直读同批改引 | `game/presentation/views/board_view.gd:258/708/932-1155`;`game/tests/unit/presentation/terrain_tile_sets_test.gd:44/45/125`;`game/probe/ui_contrast_probe.gd:146/147` | `game/presentation/views/board_view.gd:258/708/932-1155` | +| B5 | 卡① d18 触点 `lumas_out`(:998) 在瓦片族内(前置落地后随族迁移);卡① d2/d12 触点在 standee/stage/row_pacer 零交叠;iter36 wa8 触点 :730/:778/:782 在两族带外 | 卡① §1;§0 | + +## 4. 设计契约(红线,违反 = L3 停机呈报) + +🔴 **契约 1 · 零行为变更**:六 static **逐字搬运**(签名含默认参逐字一致,仅类宿主变);装载器/查表/英雄族搬运除接收者化(self→件)与缓存成员随迁外逐字一致;禁顺手改算法、禁改默认参值、禁合并/删函数;装载缓存语义不变(同板型复用/换板重载时序零变化)。 + +🔴 **契约 2 · golden 逐像素铁证**:棋盘三图+菜单 golden changed==0;任何非 0 即 L3(瓦片族与英雄渲染在像素路径上,非 0=等价性破)。 + +🔴 **契约 3 · 探针/测试缝**:探针对 BoardView 的公开面与快照断言零变化;B2 三处直引同批改引(`BoardViewScript._normalize_floor_luma`→`TilePalette.normalize_floor_luma` 同形)且改引后套件绿、断言判据零变化。 + +🔴 **契约 4 · 新件形态定案**:`tile_palette.gd`=**持缓存装配器**(七成员装载缓存+查表读缓存+六 static 算法一体;「纯静态零成员」形态经 grilling 否决);`hero_sprite.gd`=Node2D 件(`create_tween` 硬约束);两件头注 S10 形制+S9 全类型+S11 零禁用词+S13 基线零新增。 + +## 5. 范围边界 + +**做**:r2-1~r2-3+等价三证+撤销证明+产出物。 + +**不做**:board_view 其余三职责(格面渲染/输入翻译/探针面)的再拆分;瓦片算法本身的重写/优化(明度归一带与 Bible 冻结值不动);§三「其余小件」中 level_map_screen 事项;R1/R3 面。 + +## 6. 关联文档同步面 + +| 文档 | 关联点 | 同步义务 | +|---|---|---| +| [architecture](../design/engineering/architecture/architecture.md) 呈现层 D1 行/装配节 | 两新拆件登记(views 层件清单变化) | 反哺点 · 列清单(①) | +| [HUD框架](../design/gdd/systems/表现与支撑/HUD框架.md)(若逐件登记 views 清单) | 新件两行(T0 核是否列件清单) | 反哺点 · 列清单(②) | +| `待办清单.md`/`roadmap.md` | 条目拆分注记与迭代行编入笔 | 反哺点 · 列清单(③④) | +| `game/tests/`、`game/probe/` 引用点 | 实施本体 | 实施本体(可写) | + +## 7. 任务卡 + +### 卡前 · 文件结构图 + +| 文件 | 职责 | 行为改动 | +|---|---|---| +| `game/presentation/views/board_view.gd` | 两族拆出+件成员持用+调用点改引(复用既有·修改) | 非行为改动(等价迁移)→ wr1 | +| `game/presentation/views/tile_palette.gd` | **新增**:瓦片件(装载缓存+查表+六 static)(新增·新文件) | 非行为改动 → wr1 | +| `game/presentation/views/hero_sprite.gd` | **新增**:英雄精灵件 Node2D(新增·新文件) | 非行为改动 → wr1 | +| tests/probe 直引三处 | 改引(复用既有·修改) | 非行为改动 → wr1 | + +### 接口块 + +- `tile_palette.gd`:`extends RefCounted`;件内七成员装载缓存(随迁原名);`load_board_tiles(board_type: String)`(装载器——缓存语义不变;rebuild 换板时同步更新件内 board/minesweeper 引用)+查表四函数(读件内缓存+**件持棋盘引用**——`_is_wall` 依赖面,签名同形去下划线 L1)+件内 `const TILE_WALL_NAMES/TILE_FLOOR_VARIANTS/TILE_FLOOR_FMT`(常量随迁——瓦片词表唯一宿主,五处外部直读改引)+六 static(`flip_tex`/`normalize_floor_luma`/`srgb_lin`/`digit_ink_mode`/`contrast_y`/`ink_y`——签名含默认参逐字一致);board_view 持 `var _palette` 实例成员。 +- `hero_sprite.gd`:`extends Node2D`(定案——`create_tween()` Node 硬约束);持英雄精灵与状态;`update_hero/place_hero/frame_flash/notify_move/play_dig_bounce/feet/hand_point/sprite_scale`(原签名同形、接收者化;几何三量经构造注入)。 +- 最窄闸:`terrain_tile_sets_test` 单件直跑+`tools/error_scan.py` 判红口径;探针 `probe_gate.sh` 段单跑;渲染 `render_gate.sh`(golden 判据主战场)。 + +### 落位与可复用性预检 + +- **`tile_palette.gd`**——复用面:为什么不并入 ui_kit(既有 848 行静态宿主):ui_kit 是全 UI 共用色彩/字体/控件装配器,瓦片算法+装载缓存+查表是棋盘域私有——并入=跨域混装再增重;为什么留在 board_view 不可:五职责混装即本批拆因。唯一面:瓦片装载/明度算法/查表唯一宿主(B4 单源)。分层:presentation 层件零新依赖(纯 Image/Texture 计算+自有缓存)。 +- **`hero_sprite.gd`**——复用面:全库无第二英雄小人实现(standee=B 区站桩世界精灵,域与形态都不同);为什么 Node2D:`create_tween` 硬约束(B3)。唯一面:英雄小人状态机唯一宿主。分层:views 层件,几何依赖经注入。 +- **零新增算法**:全部为既有函数搬运。 + +### T0 事实坐实(≈0.25d) + +- [ ] 复核 B1-B5+外部直引复扫(grep tests/probe/sim 对瓦片全带**与英雄族八函数名与 TILE_ 三常量名**)+装载缓存成员与消费点对照(:970-978 七成员的读写点全列+查表族棋盘态读写点)。可核查结果:直引与缓存消费点清单进实现报告素材。 +- [ ] 改前读数(行数、套件通过数、golden 基线指纹)。可核查结果:基线表在案。 + +### T1 两族拆出+改引(≈1d) + +- [ ] 实装 r2-1(tile_palette 新件:装载缓存随迁+查表+六 static;board_view 件成员+调用点改引+B2 三直引点改引)→ r2-2(hero_sprite 新件:接收者化迁移+几何注入)→ r2-3(直引复扫改引)。可核查结果:board_view ≈880 行读数记录;逐字迁移核对表逐函数落位。 +- [ ] 验收步骤:全套闸按域选档全绿+棋盘三图与菜单 golden changed==0(契约 2——非 0 即停)。 + +### T2 撤销证明+产出物落位卡(≈0.25d,末卡) + +- [ ] 撤销证明一条(P0 令形态):回退 `terrain_tile_sets_test.gd:135` 改引 hunk(回指 `BoardViewScript._normalize_floor_luma` 旧址)→ 单测直跑经 error_scan 扫出 SCRIPT ERROR 红(旧址符号已随迁移消亡)→ 精确还原复绿。记录进实现报告。 +- [ ] 写齐三份产出物(实现报告含逐字迁移核对表+行数与 golden 读数+实机目检总清单〔三关瓦片明度/数字墨色/英雄移动与挖掘弹跳抽查〕/代拍板追认单/反哺点)。 +- [ ] 合并前置:独立验收审计(同卡①口径)逐行「通过」。 + +**估时合计 ≈1.5d**。编排注记:前置卡①;与 R1/R3 可同迭代分波;golden 判据重(渲染闸必跑)。 + +## 8. 验证闸 + +### 改动—验收对照表 + +| 需求点 | 改动 | 验收测试(尾缀) | 层级 | 真实入口 | 撤销证明 | +|---|---|---|---|---|---| +| wr1 两族拆出等价 | board_view 两族迁出+改引 | 全套闸绿+golden changed==0+逐字迁移核对表〔wr1〕 | 集成+渲染 | 棋盘真实渲染路径(board_shot_probe/golden)+探针 | 撤销证明一条(T2);等价三证替代逐单元撤销(等价重构申报) | +| wr2 新件合规 | 两新件 | S9/S10/S11/S13 四闸零命中〔wr2〕 | 静态扫描 | 闸真实跑批 | 回退类型/头注 → 对应闸红 | +| wr3 产出物 | T2 三份 | 件核对〔wr3〕 | — | — | 缺项红 | + +**测试削弱申报**:零(terrain_tile_sets_test 改引仅换址,断言判据零变化)。**独立验收审计**:同卡① §8.3 口径。 + +## 9. 异常分级 + +- **L1(自救)**:两新件命名与函数名前缀取舍(签名语义不变前提);几何注入面最小化写法。 +- **L2(记录继续)**:直引清单个别点改引适配;行数读数与 ≈880 的正常偏差。 +- **L3(停机呈报)**:golden changed>0(等价性破);英雄族依赖面比 B3 清单宽(接收者化不可行——判离指认失真回呈);任一套件非本批存量红;装载缓存语义无法等价随迁(消费点比 T0 清单宽)。 + +## 10. 代拍板白名单 + +1. 两新件命名与函数名前缀取舍(签名语义不变前提)。 +2. hero_sprite 几何注入的具体形态。 +3. 提交切分与 worktree 分支命名。 + +## 11. 待裁点 + +**零待裁点,开工门=U0。** 两处灰色地带推导定值(呈报可否决,随 T2 反哺点):①瓦片族落**独立 tile_palette**而非并入 ui_kit——依据=全 UI 共用宿主与棋盘私有算法+缓存的域界;备选「并入 ui_kit」淘汰依据=跨域混装+该件已 848 行再增重。②tile_palette 定形态为**持缓存装配器**(非纯静态)——依据=装载器七成员缓存的实存(B1);备选「缓存留宿主、件纯静态」淘汰依据=缓存与查表同源、拆两处即双写。hero_sprite 的 Node2D 形态非灰色地带(API 硬约束)。 + +## 12. 自审六查与 grilling 复核记录 + +**自审六查(2026-10-24 写卡会话)**:①规格覆盖——r2-1~r2-3→T1、合规/撤销/产出物→T2,指到 wr1–wr3;②步骤扫描——一步一动作一可核查结果;③类型一致性——接口块签名与 B1-B4 实查一致(v1.1 撤销 HeroState 悬空点、形态定案);④审查焦点——golden 逐像素铁证(契约 2)、B2 三直引判据不变、缓存消费点 T0 全列义务;⑤篇幅比例——零实现体;⑥落位预检——两新件不能复用既有件的理由与唯一落点已答。 + +**grilling 第一轮复核记录(2026-10-25,独立子代理对抗复核)**:判回炉三阻断——R2-A1 行数目标与清单算术断裂(v1.0 七函数 ≈156+103 行拆出后 ≈959>900,须并查表族方达 ≈887);R2-A2 `_load_board_tiles` 实持七成员缓存(:970-978),「无状态装载器」失实且与「纯静态」契约互斥;R2-A3 T2 接线变异违 P0 令。随同四应修(F1 漏 `_ink_y`/`_cell_hash`;F2 HeroState 枚举不存在于 board_view;F3 直引三处实测+iter36 陈述;F4 hero_sprite 形态缺决定性依据〔create_tween 系 Node 方法〕)。v1.1 全部折入:查表族+`_ink_y`+`_cell_hash` 并入 r2-1(≈238 行、board_view ≈880 可达)、tile_palette 定形态持缓存装配器(契约 4 重立)、hero_sprite 定 Node2D、HeroState 悬空点撤销、B3 依赖面按 grilling 清单写实、证明改撤销形态(回退改引 hunk→error_scan 红)、iter36 关系实测陈述。复核结论:三阻断闭合、四应修落位(v1.1)。 + +**grilling 第二轮复核记录(2026-10-25,同代理续核)**:总判**通过(附应修)**——三阻断真闭合(行数账自洽 1219−341≈878、缓存态契约重立、撤销证明合规)。新发现两应修:①查表族棋盘态依赖缺列(`_is_wall` 读 board 尺寸+is_revealed——接口块「读件内缓存」覆盖不了,件需棋盘引用注入);②TILE_ 常量族外部直读五处在册未列(terrain_tile_sets_test:44/45/125+ui_contrast_probe:146/147,T0 函数名 grep 兜不住)。v1.2 折入:B4 补两依赖面、接口块补棋盘引用注入与常量随迁、r2-3 直引清单+5、T0 复扫口径加常量名。零待裁点成立,卡进可编排态。 diff --git a/reports/需求卡片-重构main判离批-2026-10-24.md b/reports/需求卡片-重构main判离批-2026-10-24.md new file mode 100644 index 00000000..cf7e804e --- /dev/null +++ b/reports/需求卡片-重构main判离批-2026-10-24.md @@ -0,0 +1,169 @@ +# 需求卡片:重构 main 判离批(数据投影/只读解析回调/文案拼装三族出组合根) + +- 版本:**v1.2**(2026-10-24 v1.0 成文→2026-10-25 grilling 第一轮修订 v1.1→第二轮复核「通过(附应修)」,5 条小刀修正折入定稿;复核结论见 §12) +- 来源:[待办清单](../待办清单.md)「game 代码结构审查批——行为缺陷定点修+结构重构候选」条目之结构重构候选(2026-10-24 用户裁**五件全部立项**,本卡为 main.gd 判离件)(唯一事实源:工程直入证据指认=审计报告 [reports/审计-game代码结构审查-2026-10-24.md](审计-game代码结构审查-2026-10-24.md) §三 大文件表 main.gd 行+本写卡会话结构实查〔本卡 §3;审计「~440 行可判离/拆后 ~1070 行」读数经实查**不成立**,本卡按实码重算——§12 订正回呈〕) +- 归属:`game/presentation/main.gd`(判离源)+ `game/presentation/hud_presenter.gd`(投影/文案承接件·复用既有)+ `game/presentation/board_resolvers.gd`(**新增**·解析回调承接件) +- 排期:未编入(迭代批卡片;估时 ≈1–1.5 工作日);**前置=卡①(game 行为缺陷定点修批)先行落地**(§0) +- 开工门:U0+grilling 复核门过(§12) + +## 0. 前置依赖与并行性 + +- **第〇步核对(2026-10-24 写卡会话)**:来源条目设计面=工程直入、状态=待写卡;重构候选经 2026-10-24 用户裁全部立项另立三卡(R1/R2/R3)——工程直入重构类,凭审计 §三行号级证据放行。 +- **顺序硬约束**:卡①先落地(其 d6/d7 守卫触点在本文件 :712-717/:1486-1491);卡②(S9 注解批)若先行,其 main.gd:565 位点的注解**不随判离迁移**(:565 `build_adventure_params` 不在任何判离块——grilling 订正 v1.0 误述)。 +- **与迭代 36(已实现入库,9468c70e 等)**:iter36 卡③静态扫描归零提交含「main/_status_curios 容器参数化」——恰落在 r1-1 五函数之一的 `_status_curios` 上;**迁移基线=当前码**(iter36 已并入,无并行冲突),T0 复核现状行号。 +- 与 R2(board_view 拆件)/R3(runner 拆件)零文件交叠,可同迭代不同波或分迭代。 +- **gdstyle S13(已落地)**:新件 `board_resolvers.gd` 落在 S13 扫描面——基线零新增。 + +- **卡后换代注记(2026-10-25 补)**:迭代 35 卡②(5499eb9c)在本卡成文后合入——`main.gd` 1508→1507 行(判离块行界微漂);T0 复核按现行盘面重采,行数读数预期随动 ±5 行。 +- 探针缝约束(architecture「三屏壳与组合根句柄面」节):**探针挂载假设 `main.X` 一级句柄路径零迁移**——本卡判离的是私有函数族非句柄成员,句柄面零触碰;探针对判离函数的**五处直引**(M6)经三件薄委托保零改动(§1 r1-4)。 + +## 1. 目标与完成定义 + +**目标**:main.gd(1508 行)的三族可判离块迁出组合根——①数据投影族(五函数)②只读解析回调族(四函数)③文案拼装三件——HudPresenter 承接投影与文案、新件 board_resolvers 承接解析回调;探针直引面经薄委托零改动;判离=逐字等价迁移(零行为变更)。**行数读数(实码重算,非审计 ~440 原数)**:净判离≈140 行+三件薄委托回填≈12 行,main 收在 ≈1370–1380——审计「拆后回 ~1070」经实查不成立(其判离量读数无法复算,§12 回呈),1100 行上帝对象线的进一步下探不属本卡范围。 + +**改动单元清单**(🔴 契约 1): + +| # | 判离块(实查行界与净行数) | 去向 | 依据 | +|---|---|---|---| +| r1-1 | 数据投影族:`_status_data`(:1076-1090,15 行)/`_status_curios`(:1091-1098,8)/`_career_data`(:1126-1149,24)/`_save_select_data`(:1164-1177,14)/`_camp_data`(:1238-1241,4)——净 ≈65 行 | `hud_presenter.gd` 静态函数族(构造注入参数显式化——db/save/meta/run_scope 取值由调用侧传参);`_camp_data`/`_status_data`/`_status_curios` 三件在 main 留**薄委托**(探针直引零改动,r1-4) | 审计 §三「数据投影族」指认+HudPresenter 既有静态投影先例(feat_holdings_lines/tune_sim_report);行数=grilling 实测 | +| r1-2 | 只读解析回调族:`_identity_lookup`(:762)/`_variant_mask_lookup`(:773)/`_variant_mark_lookup`(:788)+`_debug_hero_override`(:755)——:755-798 净 ≈44 行 | 新件 `board_resolvers.gd`(构造注入 **db+debug_overlay+settlement 取值 Callable**——第二轮复核订正依赖面:`_identity_lookup` 三处读 `run_scope.settlement`〔:765 `minesweeper.current_board()`/:767 `presentation_snapshot()`/:769 `query_reward_identity()`〕、`_variant_*` 读 `db.variation.row_by_id`、`_debug_hero_override` 读 `db.hero`+调试滑条,无 meta);产出三个绑定解析 Callable+override 拼装;main :253-256 注入点改引 | 审计 §三「只读解析回调 L755-798」;解析器自成一族、依赖面窄 | +| r1-3 | 文案拼装三件:`_gp_summary_text`(:845-852)/`_bonus_summary_text`(:853-866)/`_phase_text`(:1393-1401)——净 ≈31 行 | `hud_presenter.gd` 静态函数(纯拼装零状态);`_gp_summary_text` 在 main 留**薄委托**(探针直引,r1-4) | 审计 §三「文案拼装三件」;HudPresenter 即文案拼装宿主 | +| r1-4 | 引用点:main 内部调用点改引+**探针直引五处经薄委托零改动**(`ui_click_probe.gd:74/101/112` 直调 `main._camp_data()`×3、`:789` 直调 `main._status_data()`〔iter36 sv7 断言(卡③状态面板瘦身批前缀)〕、`ui_struct_probe.gd:244` 直调 `main._gp_summary_text`——grilling 实查在册,theme_projection_test:810/825 两处注释级引用随动改写) | 三件薄委托保留(零参签名原样转发 HudPresenter 对应静态件);其余直迁 | 探针缝纪律+直引实测清单 | + +**完成定义**: + +1. 四单元落地;**行数读数记录**(main 收在 ≈1370–1380、HudPresenter/board_resolvers 各自净增行数)——读数项非停机闸(判离量以函数清单为准,§12 grilling 订正); +2. **等价三证**齐(🔴 契约 2):①逐字迁移核对表(逐函数:原行界→新落点,迁移体除接收者/参数化外逐字一致);②全套闸绿(按域选档,本批只碰 `game/`)+改前改后读数对拍;③渲染闸棋盘三图+菜单 golden 逐像素 changed==0——**resolver 族经 :253-256 注入正是棋盘徽章/数字遮蔽/角标的数据源,在像素路径上**(grilling 归因订正),等价则像素不变; +3. 新件合规:`board_resolvers.gd` 头注库内形制(S10)+全类型(S9)+零禁用词(S11)+S13 基线零新增; +4. 至少一条撤销证明记录(§7 T2——回退本批 hunk 形态);三份产出物写齐(T3)。 + +## 2. 设计事实源 + +| # | 出处 | 取用条款 | +|---|---|---| +| D1 | 工程直入证据指认=审计 §三 大文件表 main.gd 行+本会话结构实查(§3——行数与净判离量按实码重算,审计 ~440 读数不复算成立,订正回呈) | 判离块清单与行界 | +| D2 | [architecture](../design/engineering/architecture/architecture.md)「呈现层装配」节 | 组合根职责=接线(构建归屏壳/UiAssembler);探针 `main.X` 句柄路径零迁移;LAYER_CONTRACT 不动 | +| D3 | [code-conventions](../design/engineering/code-conventions.md) §1(结构纪律) | 一个职责一个文件;跨文件迁移禁顺手改写 | +| D4 | 2026-10-24 用户 P0 令 | 行为验收只走撤销证明(回退本批 hunk→红),禁注入造红 | + +## 3. 现状事实(2026-10-24 写卡会话实查+2026-10-25 grilling 实测;开工日 T0 复核行号) + +| # | 事实 | 出处 | +|---|---|---| +| M1 | main.gd 1508 行;三族行界——解析回调族 :755-798(四函数连续,净 ≈44 行)、文案两件 :845-866、投影族五函数 :1076-1241(净 ≈65 行,间隔开窗/关闭处理函数)、`_phase_text` :1393-1401;**净判离合计 ≈140 行**(grilling 逐函数实测) | `game/presentation/main.gd` 结构图谱 | +| M2 | resolver 注入点:`board_view.set_identity_resolver(_identity_lookup)`(:253)/`set_variant_mask_resolver`(:255)/`set_variant_mark_resolver`(:256)——`_build_ui` 内一次性接线;resolver 三函数经此注入是棋盘每格渲染数据源(徽章/数字遮蔽/角标) | `game/presentation/main.gd:253-256` | +| M3 | 解析回调族读态面(两轮复核订正):`_identity_lookup`(:762-772) 三处读 `run_scope.settlement`(:765 `minesweeper.current_board()`/:767 `presentation_snapshot()`/:769 `query_reward_identity()`);`_variant_mask/mark_lookup` 读 `db.variation.row_by_id`;`_debug_hero_override`(:755) 读 `db.hero`+`debug_overlay` 滑条,无 meta | `game/presentation/main.gd:755-798` | +| M4 | HudPresenter 现役形态:static 函数族(`refresh` :12 起、`feat_holdings_lines`/`tune_sim_report` 等),纯投影/拼装零成员态——承接 r1-1/r1-3 的先例形态 | `game/presentation/hud_presenter.gd` | +| M5 | 投影族消费面:`_status_data`→status_panel、`_career_data`→career_overlay、`_save_select_data`→save_select_overlay、`_camp_data`→camp_overlay、`_status_curios`→`_status_data` 内部 | `game/presentation/main.gd:1076-1241` | +| M6 | **探针/测试直引五处在册**(grilling 实查):`ui_click_probe.gd:74/101/112`(`main._camp_data()`×3)、`:789`(`main._status_data()` sv7 直调八键断言,iter36 wc7 刚落)、`ui_struct_probe.gd:244`(`main._gp_summary_text`);另 `theme_projection_test.gd:810/825` 两处注释级同构引用 | 上列 file:line | +| M7 | 卡①触点 :712-717/:1486-1491(先落地);`_tune_sim` 构造 :393;卡②触点 :565 **不在判离块** | §0 | + +## 4. 设计契约(红线,违反 = L3 停机呈报) + +🔴 **契约 1 · 零行为变更**:判离=逐字等价迁移——函数体除「成员访问改显式参数/接收者」外**逐字一致**;禁顺手重构、禁改名(承接件内函数名保持原名,静态化所需参数化最小化);`LAYER_CONTRACT` 与一切句柄成员零触碰;三件薄委托签名与现 main 私有函数逐字同形(`_gp_summary_text` 带参逐字转发——第二轮复核措辞订正)。 + +🔴 **契约 2 · 等价三证**:逐字迁移核对表+全套闸绿+golden(棋盘三图+菜单)逐像素 changed==0,三证缺一不算完成;changed>0 即 L3(resolver 在像素路径上——归因见完成定义 2)。 + +🔴 **契约 3 · 探针缝零迁移**:`main.X` 一级句柄路径与探针既有断言零变化;M6 五处直引经薄委托零改动(探针文件本批零改动);theme_projection_test 两处注释级引用随动改写(注释面非断言)。 + +🔴 **契约 4 · 新件合规**:`board_resolvers.gd` 一文件一职责(S10 头注+库内 VERTICAL SLICE 首行形制、S9 全类型、S11 零禁用词、S13 基线零新增);禁把判离块散落多处(r1-2 整族一个文件)。 + +🔴 **契约 5 · LAYER_CONTRACT 与模态登记零触碰**:判离块不含层序/登记逻辑;`_apply_layer_contract` 不动。 + +## 5. 范围边界 + +**做**:r1-1~r1-4 四单元+等价三证+撤销证明一条+产出物。 + +**不做**:main.gd 其余部分的继续拆分(`_build_ui`/信号处理族/开窗关闭处理族留驻——审计仅指认三族;1100 行线的进一步下探待后续批);R2/R3 各自文件;卡①缺陷面;§三「其余小件」中 main.gd 死变量/confirm 旁路/settle_batch 空判(未立项,留审计在册)。 + +## 6. 关联文档同步面 + +> 迭代批禁写 `design/` 与根规则文件;同步义务=列进 `reports/iter-反哺点-<完成日>.md` 归文档批。 + +| 文档 | 关联点 | 同步义务 | +|---|---|---| +| [architecture](../design/engineering/architecture/architecture.md) 呈现层 D1 行/呈现层装配节 | 新增 `board_resolvers.gd` 拆件+HudPresenter 职责扩列(投影/文案承接)=模块清单变化;架构文档 :149/151「17 项」计数漂移属条目④机械对齐件(零触碰、反哺点并呈) | 反哺点 · 列清单(①新件登记+HudPresenter 职责句扩列) | +| [HUD框架](../design/gdd/systems/表现与支撑/HUD框架.md)(若逐件登记呈现层清单) | 新件一行(T0 核该文档是否列件清单) | 反哺点 · 列清单(②) | +| `待办清单.md`/`roadmap.md` | 条目拆分注记与迭代行编入笔 | 反哺点 · 列清单(③④) | +| `game/` 探针/测试引用点 | 实施本体(本批预期探针零改动,注释级两处随动) | 实施本体(可写) | + +## 7. 任务卡 + +> 回归措辞:里程碑步出口一次全套闸;波内修复只做最窄确认。 + +### 卡前 · 文件结构图 + +| 文件 | 职责 | 行为改动 | +|---|---|---| +| `game/presentation/main.gd` | 三族判离+三件薄委托+调用点改引(复用既有·修改) | 非行为改动(等价迁移)→ wr1 | +| `game/presentation/hud_presenter.gd` | r1-1/r1-3 承接(复用既有·扩静态族) | 非行为改动 → wr1 | +| `game/presentation/board_resolvers.gd` | **新增**:r1-2 解析回调族承接(新增·新文件) | 非行为改动 → wr1 | +| `game/tests/unit/presentation/theme_projection_test.gd` | 两处注释级引用随动改写(复用既有·改注释) | 非行为改动 → wr1 | + +### 接口块 + +- `board_resolvers.gd` 形态(命名 L1 可调):`extends RefCounted`;`_init(db_ref, debug_overlay_ref, settlement_getter: Callable)`(依赖面=M3 实查——db+调试滑条+settlement 活态取值〔`_identity_lookup` 三读 settlement〕;无 meta);产出 `identity_lookup(c: Vector2i) -> Variant`/`variant_mask_lookup(c: Vector2i) -> bool`/`variant_mark_lookup(c: Vector2i) -> String`/`debug_hero_override() -> Dictionary`(签名与现 main 私有函数逐字同形,返回类型按现体实态补全)。 +- HudPresenter 承接函数命名:保持原名去下划线入静态族(`status_data`/`status_curios`/`career_data`/`save_select_data`/`camp_data`/`gp_summary_text`/`bonus_summary_text`/`phase_text`),参数表显式化(取值面=M5 消费链:db/save/meta/runner 状态由调用侧传参);命名差异属 L1。 +- 三件薄委托(main 内保留,签名逐字同形零参转发):`_camp_data()`/`_status_data()`/`_gp_summary_text()`——探针直引零改动的承载面;`_status_curios` 为 `_status_data` 内部调用,随委托链走(grilling 行数账注:委托回填 ≈12 行)。 +- 最窄闸:探针族=`tools/probe_gate.sh` 对应段单跑;单测=`--script` 单件直跑+`tools/error_scan.py` 判红口径;渲染=`tools/render_gate.sh`。 + +### 落位与可复用性预检 + +- **`board_resolvers.gd`(新文件)**——复用面:为什么不并入 HudPresenter:解析回调是**有态绑定件**(构造注入 db/滑条、产出绑定 Callable),HudPresenter 是无态静态族——混装破坏两者形态纪律;为什么不留在 main:审计判离指认+组合根只接线原则。唯一面:board_view 三个 resolver 的唯一供给源(注入点 :253-256 单点)。分层与交互面:presentation 层内部件,零 core 依赖(读 db 与调试滑条),零新跨层出口。 +- **HudPresenter 扩列**——复用面:既有静态投影/拼装宿主(M4),扩列即复用不新立;唯一面:投影/文案承接后仍是唯一宿主(与 board_resolvers 职责互斥:静态投影 vs 有态绑定)。 +- **三件薄委托**——为什么留:探针五处直引(M6)零改动的最小承载;备选「探针改引」淘汰依据=探针缝纪律(探针文件零改动优先)。 +- **零新增函数体**:全部迁移体为既有函数逐字搬运(薄委托为三行转发体)。 + +### T0 事实坐实(≈0.25d) + +- [ ] 复核 M1-M7(重点:M3 `_identity_lookup` 取值链实读;M6 五处直引现址复核;行号漂移)。可核查结果:直引清单+漂移声明进实现报告素材。 +- [ ] 改前读数(main.gd 行数、探针/单测/golden 通过数)。可核查结果:基线表在案。 + +### T1 三族判离+改引(≈0.75d) + +- [ ] 实装 r1-2(board_resolvers 新件+:253-256 注入改引)→ r1-1/r1-3(HudPresenter 扩列+main 调用点改引)→ r1-4(三件薄委托+theme_projection_test 注释随动)。可核查结果:`git diff` 迁移体逐字一致(核对表逐函数行界对);行数读数记录(预期 ≈1370–1380)。 +- [ ] 验收步骤:全套闸按域选档跑一次全绿+棋盘三图与菜单 golden changed==0(契约 2——非 0 即 L3)+探针全绿(M6 五处直引零改动即绿)。 + +### T2 撤销证明+产出物落位卡(≈0.25d,末卡) + +- [ ] 撤销证明一条(P0 令形态——只回退本批 hunk):回退 HudPresenter 承接件任一函数 hunk(如 `gp_summary_text` 承接体)→ main 委托/调用点引用未定义符号 → 单测/探针直跑输出经 `tools/error_scan.py` 扫出 SCRIPT ERROR 红 → 精确还原复绿。记录进实现报告。 +- [ ] 写齐三份:`reports/iter-实现报告-<完成日>.md`(逐字迁移核对表+行数读数+golden 读数+实机目检总清单〔三屏开合与状态面板/生涯/选档/营地投喂抽查〕)/代拍板追认单/反哺点(§6 各条落点)。 +- [ ] 合并前置:独立验收审计(未参与实现的会话/子代理,只读本卡/diff/测试件+抽查迁移体逐字一致性)报告逐行「通过」。 + +**估时合计 ≈1.25d**。编排注记:前置卡①落地后开工;估时低于 iter-plan 硬下限,与 R2/R3 或他卡同批编入。 + +## 8. 验证闸 + +### 改动—验收对照表 + +| 需求点 | 改动 | 验收测试(尾缀) | 层级 | 真实入口 | 撤销证明 | +|---|---|---|---|---|---| +| wr1 三族判离等价 | main.gd 三族迁出+薄委托+改引 | 全套闸绿+golden changed==0+逐字迁移核对表〔wr1〕 | 集成+渲染 | 探针真实驱动路径(五直引零改动)+面板真实投喂链 | 撤销证明一条(T2——回退承接 hunk→error_scan 红);等价三证替代逐单元撤销(等价重构申报) | +| wr2 新件合规 | board_resolvers.gd | S9/S10/S11/S13 四闸零命中〔wr2〕 | 静态扫描 | 闸真实跑批 | 回退头注/类型 → 对应闸红 | +| wr3 产出物 | T2 三份 | 件核对〔wr3〕 | — | — | 缺项红 | + +**测试削弱申报**:零(既有断言零改动——theme_projection_test 两处为注释级随动)。**独立验收审计**:同卡① §8.3 口径。 + +## 9. 异常分级 + +- **L1(自救)**:承接件命名(去下划线入静态族等);参数化的最小形态。 +- **L2(记录继续)**:行数读数与 ≈1370–1380 预期的正常偏差(判离量以函数清单为准)。 +- **L3(停机呈报)**:迁移体无法逐字等价(依赖面比 M3 宽——判离指认失真,回呈审计结论);golden changed>0;任一探针/套件出非本批存量红(含 M6 直引处变红)。 + +## 10. 代拍板白名单 + +1. board_resolvers.gd 与承接函数命名(接口块签名语义不变前提)。 +2. 参数化取值的具体形态。 +3. 提交切分与 worktree 分支命名。 + +## 11. 待裁点 + +**零待裁点,开工门=U0。** 一处灰色地带推导定值:解析回调族落**新件**而非 HudPresenter——依据=有态绑定件与无态静态族的形态纪律(预检节);备选「并入 HudPresenter」淘汰依据=形态混装。另一处:探针直引面走**薄委托**而非探针改引——依据=探针缝纪律(探针零改动优先);备选「探针改引」淘汰依据=改探针引入额外验证面。两处均呈报可否决(随 T2 反哺点)。 + +## 12. 自审六查与 grilling 复核记录 + +**自审六查(2026-10-24 写卡会话)**:①规格覆盖——r1-1~r1-4→T1、合规/撤销/产出物→T2,指到 wr1–wr3;②步骤扫描——一步一动作一可核查结果;③类型一致性——接口块签名与 M1-M6 实查一致;④审查焦点——探针直引五处(M6→薄委托)、golden 归因(resolver 在像素路径——契约 2)、iter36 迁移基线换代;⑤篇幅比例——重构卡零实现体;⑥落位预检——新件唯一性、HudPresenter 复用、薄委托理由已答。 + +**grilling 第一轮复核记录(2026-10-25,独立子代理对抗复核)**:判回炉两阻断——R1-A1 行数目标与判离清单算术断裂(实查净判离 ≈140 行,≤1100 差约 260 行不可达,审计 ~440/1070 读数自身不成立);R1-A2 T2「接线变异」违 2026-10-24 P0 令。随同五应修(F1 M6 直引五处实测在册;F2 依赖面错位〔meta→实为 db〕;F3 golden 归因失真〔resolver 在像素路径〕;F4 :565 不随迁误述;F5 iter36 已入库且 `_status_curios` 参数化交叠)。v1.1 全部折入:行数账按实码重算(≈1370–1380 读数记录、1100 线下探移出范围、审计读数订正回呈)、证明改撤销形态(回退承接 hunk→error_scan 红)、依赖面/golden 归因/:516 直引清单/iter36 基线换代全部订正、薄委托方案定案(§11 呈报)。复核结论:两阻断闭合、五应修落位。 + +**grilling 第二轮复核记录(2026-10-25,同代理续核)**:总判**通过(附应修)**——第一轮两阻断真闭合、行数账自洽、证明形态合规、薄委托方案成立。新发现一条应修(接口块 v1.1 修订时把 v1.0 原本正确的 settlement 注入弄丢——`_identity_lookup` 三处读 `run_scope.settlement`,两参注入面无法支撑)+两条建议(M6「wc7」实为 sv7;「零参转发」对带参 `_gp_summary_text` 不精确)。v1.2 折入:接口块/M3/r1-2 恢复 settlement 取值面(三参注入)、sv7 订正、「签名逐字同形转发」措辞。零待裁点成立,卡进可编排态。 diff --git a/reports/需求卡片-重构runner拆件与断点schema单点批-2026-10-24.md b/reports/需求卡片-重构runner拆件与断点schema单点批-2026-10-24.md new file mode 100644 index 00000000..9076635e --- /dev/null +++ b/reports/需求卡片-重构runner拆件与断点schema单点批-2026-10-24.md @@ -0,0 +1,173 @@ +# 需求卡片:重构 runner 拆件与断点 schema 单点批(CurioDeck+商店件+断点字段单源) + +- 版本:**v1.2**(2026-10-24 v1.0 成文→2026-10-25 grilling 第一轮修订 v1.1〔两阻断+五应修折入〕→第二轮复核「通过(附应修)」,两条应修(行数读数按新边界重算/契约 5 接线行例外)折入定稿;复核结论见 §12) +- 来源:[待办清单](../待办清单.md)「game 代码结构审查批——行为缺陷定点修+结构重构候选」条目之结构重构候选(2026-10-24 用户裁**五件全部立项**,本卡收 adventure_runner 令牌池拆件+level_runner 商店拆件+断点 schema 单点化三件)(唯一事实源:工程直入证据指认=审计报告 [reports/审计-game代码结构审查-2026-10-24.md](审计-game代码结构审查-2026-10-24.md) §三 大文件表 adventure_runner/level_runner 行+断点 schema 段+本写卡会话结构实查〔本卡 §3——审计「断点三处重复登记」经实查**save 侧零键登记**,「三处」订正为写/读两侧,§12 回呈〕) +- 归属:`game/core/adventure_runner.gd`(令牌池拆出源)+ `game/core/level_runner.gd`(商店拆出源)+ `game/core/curio_deck.gd`(**新增**·令牌池件)+ `game/core/shop_session.gd`(**新增**·商店件,命名 L1 可调)+ 断点字段单源(宿主内 const,§11 定值)+ 断点域测试面 +- 排期:未编入(迭代批卡片;估时 ≈2–2.5 工作日);**前置=卡①先行落地**(其 d11 表读触点在商店族内) +- 开工门:U0+grilling 复核门过(§12) + +## 0. 前置依赖与并行性 + +- **第〇步核对(2026-10-24 写卡会话)**:来源条目设计面=工程直入、状态=待写卡;重构候选 2026-10-24 用户裁全部立项——工程直入重构类,凭审计 §三行号级证据放行。 +- **顺序硬约束**:卡①先行(d11 表读替换落在 `_spend_discounted`/`pool_price` 内——先修后迁,迁移体带上修好的表读形态);卡②注解若先行随迁。R1/R2 零文件交叠,可并行波。 +- **与迭代 36(已实现入库)**:iter36 卡C wc2 落「断点快照喂入→`resume_adventure` :151-154 真链回填、run_ledger.bonus.atk==5」续玩断言(ui_click_probe)——**现役最强断点行为回归,纳入 T0 基线与本批回归面**;本批触 `resume_adventure` 读写侧,该断言必须全程绿。 + +- **卡后换代注记(2026-10-25 补)**:迭代 35 卡②(5499eb9c)在本卡成文后合入,触两卡面:①断点键集换代——写侧 `template_pick` 键退役、换代差标记 `graph_gen_v2: 1`(adventure_runner:1014),`resume_adventure` 增缺键显式拒绝(:146-149);**顶层键数仍 27、键清单以 T0 现行盘面重采为准**(本卡 A3 清单按此换代,读侧策略词表不变——graph_gen_v2 归 plain 类缺省);`_start_current_level` 二次消费面相应变化(template_pick 出列,T0 重列);②`level_runner.gd` 行号漂移(商店族整体 −12 行上下,函数清单不变);③`resume_adventure` 104→108 行(代差检查四行入读侧——r3-3 单源化覆盖面含它)。 +- **断点兼容硬约束**:断点单点化**不改任何键名/缺省值/回退语义**——旧档读取行为逐位不变;`breakpoint_resume_test`+iter36 wc2 断言是回归主线。 +- **gdstyle S13(已落地)**:两新 core 件落在 S13 扫描面——基线零新增。 + +## 1. 目标与完成定义 + +**目标**:①adventure_runner(1049 行)的**池与判定面**(令牌多重集/抽签/cap/凭证判定/库存查询,≈340 行)拆出为 `curio_deck.gd`——**停点编排与兑付推进面留宿主**(边界见 r3-1);②level_runner(1012 行)的商店族(21 函数,≈260 行)拆出为 `shop_session.gd`;③断点 schema 写/读两侧重复登记单点化为字段登记单源——三件全零行为变更,断点档与续玩行为逐位不变。 + +**改动单元清单**(🔴 契约 1): + +| # | 单元(实查) | 改法 | 依据 | +|---|---|---|---| +| r3-1 | **迁出面**(池与判定):成员 `_curio_deck`(:60)/`_curio_taken`(:59)/`_feat_ticket_pity`(:67)/`_feat_ticket_sold`(:68)/`_feat_ticket_appeared`(:69)+函数 `_build_curio_deck`(:265)/`_token_identity`(:279)/`_find_token`(:346)/`pool_stock`(:461)/`_consume_pool_purchase`(:473)/`_pool_stock_snapshot`(:484)/`_consume_deck_token`(:440-452,grilling 补入——纯池摘牌,不迁则件内池被宿主直改=双写)/`_draw_curio_candidates`(:669)/`_curio_cap_ok`(:757)/`_feat_ticket_consider`(:772)+`_reset_curio_state`(:247) 的池半(件内 `reset()` 承载,宿主复位停点态)。**留宿主面**(停点编排与兑付推进):`skip_empty_curio`(:334)/`pick_curio`(:361)/`_feat_payload_keys`(:381)/`_apply_curio_pick`(:393)/`_finish_curio_pick`(:494)/`node_curio_candidates`(:513)/`present_node_curio`(:518)/`apply_node_curio`(:525)/refresh 族(:556-641,读写 `_attune_refresh_x`/`_refresh_stock`/`_node_curio_pending` 停点态——**断点时点 :634 住在 `_refresh_curio_common` 体内**)/`_finish_node_bonus`(:643)/`_redeem_feat_ticket`(:789,兑付推进——**断点时点 :803 住其体内**);另 `feats_held`(:453) 读宿主 `_feats` 留驻(持有集查询,非池面) | 拆出 `curio_deck.gd`:令牌多重集+kind 计数+凭证三计数+抽签/cap/凭证判定入件;宿主公开面委托保持原签名(外部调用面零变化);AdventureRunner 行数读数预期 **≈850±10**(第二轮按 v1.1 边界清单重算:迁出 ≈211 行+委托与 Callable 绑定回填 ≈15–25——第一轮 709 系「只留 refresh 族」旧口径,随边界扩到兑付推进族作废;**读数项非停机闸**) | 审计 §三「令牌池子系统(构建/抽签/cap/凭证,~500 行)」指认——**边界经 grilling R3-A1 重划**(全族含 refresh 迁则断点时点被扰动、违零变更;拆「池与判定」不拆「停点编排」) | +| r3-2 | 商店族 21 函数(`shop_products`(:753)/`current_price`(:769)/`_spend_discounted`(:777)/`purchased_tier`(:783)/`buy_product`(:789)/`player_request_refresh`(:812)/`set_shop_pool`(:823)/`_shop_open_pool`(:832)/`_shop_draw_shelf`(:843)/`pool_stock`(:880)/`pool_price`(:891)/`shop_shelf`(:905)/`shop_refresh_free_left`(:932)/`_attune_shop_refresh_bonus`(:939)/`shop_refresh_cost`(:948)/`shop_refresh`(:953)/`spend_gp`(:967)/`buy_pool_item`(:977)/`is_shop_pool`(:1002)/`shop_bp_shelf`(:1007)/`shop_bp_visit_buys`(:1011))+成员 `_shop_*`(:37/:49-56)——**以函数清单为准**(:715-1011 区间内 `grant_curio_gp`/`continue_after_node` 等非商店函数不迁) | 拆出 `shop_session.gd`:双模式货架/定价/购买/刷新入件(db 注入+`set_shop_pool` 既有四参 Callable 面沿用,缺省值 `Callable()` 逐字对齐 :823);LevelRunner 公开面委托保持原签名;钱包原语 `_gain_wallet`/`close_wallet` 留宿主(`spend_gp` 随件迁、经回调落宿主账本——§11 定值);level_runner 读数预期 ≈780 | 审计 §三「商店子系统(legacy+pool 双模式,~260 行)可拆」 | +| r3-3 | 断点 schema **写/读两侧**(grilling 订正:审计「三处」的 save 侧经实查**零键登记**——:218-219/:298 只置空字典、:595-607 四口值拷贝,无字段枚举=空派生不动):写侧 `_snapshot_breakpoint`(:990-1034,**27 顶层键**——bonus 含 4 子键;v1.0「28 键」计数订正);读侧两消费位=`resume_adventure`(:140-246)+`_start_current_level` 二次消费(`node_id`/`visited_ids`/`template_pick`/`aux_items` 四键经 `_resume_bp` 在 :854-860/:875-877,grilling 补查——v1.0 漏记) | 单源化:宿主内 `const BREAKPOINT_FIELDS`(键→登记项:写侧取值器+读侧回退策略标签)驱动写侧拼装与**两处**读侧回退;**读侧回退策略按实语义逐键归类**(grilling R3-F2 五类:plain 缺省/null 类型守卫保持既有态〔bonus_deck/adventure_counters/attunement_charges :174/:223/:241〕/双向合并〔bonus_taken :162-165〕/钳制〔bonus 子键 maxi(0)、hero_hp :192-193〕/参数面回退〔attune_refresh_y :205〕/令牌化迁移〔bonus_pending.candidates :230-240〕)——`{default, deep}` 双字段表达不了的面按策略标签登记;save 侧经核零改动 | 审计 §三「断点 schema 三处重复登记,每加一字段改三处」——写/读两侧单源化即消散弹面;save 侧论断不成立(§12 回呈) | + +**完成定义**: + +1. 三单元落地;**行数读数记录**(adventure_runner ≈710–730/level_runner ≈780/两新件净行数——读数项非停机闸,判离量以函数清单为准); +2. 等价三证齐:①逐字迁移核对表(两拆件族)+断点单源对照表(**27 顶层键逐键:缺省值/回退策略改前改后一致**);②全套闸绿+读数对拍;③断点专项:`breakpoint_resume_test` 绿+iter36 wc2 续玩断言(ui_click_probe)绿+新增断言「单源键集==写侧字典键集」(wr2——判据从写侧实拼字典推导); +3. 两新件+单源合规(S9/S10/S11/S13+core 域 S1-S5:零 UI 零 await 零裸随机零表直读); +4. 至少两条撤销证明记录(T3);三份产出物写齐(T3)。 + +## 2. 设计事实源 + +| # | 出处 | 取用条款 | +|---|---|---| +| D1 | 工程直入证据指认=审计 §三 adventure_runner/level_runner 行+断点 schema 段+本会话结构实查(§3——边界经 grilling 重划、save 侧论断订正) | 三单元清单与边界 | +| D2 | [architecture](../design/engineering/architecture/architecture.md) 核心层纪律+「战斗域装配现状」节 RunLedger 先例 | core 件零引擎依赖;共享账经显式类/引用不隐式——r3-1 宿主记账回调化的依据 | +| D3 | [code-conventions](../design/engineering/code-conventions.md) §1/§2.4 | 一文件一职责;状态字段语义显式(断点单源=语义显式化) | +| D4 | [存档序列化](../design/gdd/systems/表现与支撑/存档序列化.md) | 断点域契约(SCHEMA_VERSION+快照锚)——键集冻结的规范面 | +| D5 | 2026-10-24 用户 P0 令 | 行为验收只走撤销证明,禁注入造红 | + +## 3. 现状事实(2026-10-24 写卡会话实查+2026-10-25 grilling 实测;开工日 T0 复核行号) + +| # | 事实 | 出处 | +|---|---|---| +| A1 | adventure_runner 1049 行;池与判定面(r3-1 迁出清单)与停点编排面(留宿主清单)的分界依据:refresh 族读写 `_attune_refresh_x`(:49)/`_refresh_stock`(:53)/`_node_curio_pending`(:63) 停点态、`_finish_curio_pick`/`_finish_node_bonus` 写 run_ledger.bonus 与 ledger、`_redeem_feat_ticket` 兑付推进——**断点五时点之三住在留宿主函数体内**(:506 在 `_finish_curio_pick`、:634 在 `_refresh_curio_common`、:803 在 `_redeem_feat_ticket`) | `game/core/adventure_runner.gd:49/53/63/247-810/990-1034` | +| A2 | level_runner 1012 行;商店族 21 函数清单(r3-2,函数清单为准);`set_shop_pool`(:823) 四参注入面(deck 引用+consume/ticket/cap 三 Callable,后两参缺省 `Callable()`)=拆件回调面的现成形态;钱包边界=`_gain_wallet`(:738)/`close_wallet`(:745) 宿主留、`spend_gp`(:967) 只服务商店扣款 | `game/core/level_runner.gd:37/49-56/715-1011` | +| A3 | 断点写侧 **27 顶层键**(:1001-1030 逐键实查:level_index/node_id/hero_hp/bonus{atk,max_hp,armor_class,ap 四子键}/gp/shop_purchased/shop_shelf/shop_visit_buys/attunement_charges/template_pick/master_seed/rng_counter/keystone/visited_ids/aux_items/bonus_taken/bonus_deck/relics/bonus_pending/feat_ticket_pity/feat_ticket_sold/adventure_counters/vision_counter_used/refresh_stock/attune_refresh_y/bonus_result/keystone_drops);读侧消费**两处**:`resume_adventure`(:140-246——五类回退策略实语义见 r3-3)+`_start_current_level` 经 `_resume_bp` 四键二次消费(:854-860/:875-877);save 侧 :218-219/:298/:595-607 **零键登记**(空字典与值拷贝) | `game/core/adventure_runner.gd:140-246/990-1034`;`game/core/save_manager.gd:218-219/298/595-607` | +| A4 | 断点五时点集::506/:634/:803/:933/:982——**全部住在留宿主函数体内**(A1),拆件不扰动时点形态 | `game/core/adventure_runner.gd:506/634/803/933/982` | +| A5 | 外部调用面:LevelRunner 商店/池面消费方=main.gd+camp_shop_presenter/curio_interact_controller(呈现侧)+level_shop_economy_test/curio_refresh_paid_test/pool_case_d_test 等;AdventureRunner 令牌面消费方=main/控制器+断点域测试——公开签名零变化=外部零改动的依据;iter36 wc2 断点续玩断言在 `ui_click_probe`(断点快照喂入→resume 真链→run_ledger.bonus.atk==5) | T0 直引清单化;`game/probe/ui_click_probe.gd`(iter36 wc2 段) | +| A6 | 卡① d11 触点 `pool_price`(:891)/`_spend_discounted`(:777) 在商店族内(先修后迁);`shop_bp_shelf`/`shop_bp_visit_buys`(:1007/:1011) 是断点附随取数面(随商店件迁、断点写侧经其公开面取数不变) | 卡① §1 d11;`game/core/level_runner.gd:1007-1011` | + +## 4. 设计契约(红线,违反 = L3 停机呈报) + +🔴 **契约 1 · 零行为变更+公开面零变化**:两拆件族的**公开函数签名逐字保持**(宿主委托层承载);迁移体逐字一致(接收者化/参数化最小);断点 **27 顶层键**的键名/缺省值/回退策略逐键冻结(对照表落实现报告,读侧五类策略逐键归类登记);禁顺手改任何判定/缺省/顺序。 + +🔴 **契约 2 · core 域纪律**:两新件 core 合规全过——零 UI 引用(S4)、零 await、随机只经注入参数面(S1/S2——`_feat_ticket_consider(rg: RandomNumberGenerator)` 现形态 :772 随迁不改)、零表直读(数值经注入 db,S3/S5);头注 S10 形制+S13 基线零新增。 + +🔴 **契约 3 · 断点专项回归**:`breakpoint_resume_test` 全绿+iter36 wc2 续玩断言全程绿+新增「单源键集==写侧键集」断言一条(wr2);五时点集(A4)零增减零迁移。 + +🔴 **契约 4 · 等价三证**(§1 完成定义 2)+撤销证明至少两条(T3——P0 令形态)。 + +🔴 **契约 5 · 范围收口**:只拆两族+单源化+**一条必改接线行**——`adventure_runner.gd:883-884` `_runner.set_shop_pool(_curio_deck, _consume_pool_purchase, _feat_ticket_consider, _curio_cap_ok)` 四标识符全在迁出面,该行随拆件改引(经宿主委托/件成员取值,参数序不变);除此与 `_settle`/`_start_current_level`/节点推进等其余面不动;`_place_variants` 搬迁(审计 §三附带小件,battle_orchestrator 面)不在本卡。 + +## 5. 范围边界 + +**做**:r3-1~r3-3+断点键集断言+等价三证+撤销证明+产出物。 + +**不做**:battle_orchestrator 的段类再拆与 `_place_variants` 搬迁(未立项,留审计在册);present 层三控制器写调用归属(条目④裁点);R1/R2 面;卡①缺陷面。 + +## 6. 关联文档同步面 + +| 文档 | 关联点 | 同步义务 | +|---|---|---| +| [architecture](../design/engineering/architecture/architecture.md) 核心层/功能层模块归属表 | 两新 core 件登记(CurioDeck/ShopSession 归属行)——与条目④「模块归属对齐」并批落文 | 反哺点 · 列清单(①:登记行草案,落文归④批统一裁决) | +| [存档序列化](../design/gdd/systems/表现与支撑/存档序列化.md) | 断点键集零变化——单源化不改契约;T0 核其是否逐键列清单(列则注记单源载体址) | 反哺点 · 列清单(②,预期零改动注记) | +| [秘宝](../design/gdd/systems/跨局循环/秘宝.md)(若记令牌池结构)/商店相关 GDD | 结构载体址变化、机制语义零变化(T0 核) | 反哺点 · 列清单(③,预期载体址注记) | +| `待办清单.md`/`roadmap.md` | 条目拆分注记与迭代行编入笔 | 反哺点 · 列清单(④⑤) | + +## 7. 任务卡 + +### 卡前 · 文件结构图 + +| 文件 | 职责 | 行为改动 | +|---|---|---| +| `game/core/adventure_runner.gd` | 池与判定面拆出+委托层+断点写侧改单源驱动(复用既有·修改) | 非行为改动 → wr1 | +| `game/core/level_runner.gd` | 商店族拆出+委托层(复用既有·修改) | 非行为改动 → wr1 | +| `game/core/curio_deck.gd` | **新增**:令牌池件(新增·新文件) | 非行为改动 → wr1 | +| `game/core/shop_session.gd` | **新增**:商店件(新增·新文件) | 非行为改动 → wr1 | +| 断点字段单源(adventure_runner 内 `const BREAKPOINT_FIELDS`——§11 定值) | 27 键登记单源+写/读派生(新增·常量) | 非行为改动 → wr1 | +| `game/tests/unit/core/breakpoint_resume_test.gd` | 键集断言一条(复用既有·增断言) | 断言面 → wr2 | +| 外部直引点(A5 清单) | 零改动预期(公开签名不变)——T0 核 | — | + +### 接口块 + +- `curio_deck.gd`:`extends RefCounted`;`_init(db: ConfigDb)`;持令牌多重集+`_curio_taken`+凭证三计数;方法=迁出清单去下划线同形(`build/token_identity/find_token/pool_stock/consume_pool_purchase/stock_snapshot/consume_deck_token/draw_candidates/cap_ok/feat_ticket_consider/reset`);`feat_ticket_consider(rg: RandomNumberGenerator) -> bool` 形态随迁不改;宿主记账经回调注入(形态 L1)。 +- `shop_session.gd`:`extends RefCounted`;`_init(db: ConfigDb)`;`set_pool(deck_ref: Array, consume_handler: Callable, ticket_consider: Callable = Callable(), cap_check: Callable = Callable())`(与 :823 逐字对齐含缺省值);方法=商店 21 函数清单同形(`spend_gp` 随迁、出账经回调落宿主 `_gain_wallet`)。 +- 断点单源:`const BREAKPOINT_FIELDS: Dictionary`(键→{写侧取值键路径+读侧策略标签〔plain/keep_on_null/dual_merge/clamp/params_fallback/token_migration 六类〕})+写侧拼装/读侧回退两派生段。 +- 最窄闸:`breakpoint_resume_test`/`level_shop_economy_test`/`curio_refresh_paid_test`/`pool_case_d_test` 单件直跑+`tools/error_scan.py` 判红口径;系统测试 `python tools/run_system_test.py`;探针 `ui_click_probe`(iter36 wc2 断言)。 + +### 落位与可复用性预检 + +- **`curio_deck.gd`**——复用面:全库令牌池逻辑唯一宿主即 adventure_runner 私有族(grep 实查),无第二实现;为什么不留在宿主:审计指认自成一体+1049 行超线。唯一面:令牌多重集与凭证计数唯一持有者(断点 `bonus_deck`/`bonus_taken`/`feat_ticket_pity`/`feat_ticket_sold` 数据源单点)。分层:core 件零引擎依赖;db 注入;随机经注入 rg 参数面(现状形态)。 +- **`shop_session.gd`**——复用面:`set_shop_pool` 既有 Callable 注入面即拆件接口的现成形态(零新造协议);无第二商店实现。唯一面:货架/定价/购买/刷新唯一宿主(断点 `shop_shelf`/`shop_visit_buys` 数据源)。分层:core 件;钱包原语留宿主(账本单点,`spend_gp` 经回调)。 +- **断点单源 const 随宿主**——复用面:为什么不上独立件:27 键多数取宿主态(20+ 键取宿主成员/嵌套),独立件要把宿主全量开洞——const 随宿主+两派生段最小;备选「独立 schema 件」淘汰依据=开洞面>收益。唯一面:键登记唯一处(写侧+两读侧三消费全派生)。 +- **零新增逻辑**:全部为搬运+登记单源化。 + +### T0 事实坐实(≈0.25d) + +- [ ] 复核 A1-A6(重点:A3 27 键缺省/策略表逐键摘录;A5 外部直引清单;r3-1 边界函数逐一确认归属——尤其 `pick_curio`/`skip_empty_curio` 停点面归属复核)。可核查结果:边界判定表+27 键策略表进实现报告素材。 +- [ ] 改前读数(行数、断点/商店/令牌域套件通过数、ui_click_probe iter36 wc2 段绿读数)。可核查结果:基线表在案。 + +### T1 商店拆件+断点单源(≈0.75–1d) + +- [ ] 实装 r3-2(shop_session 拆出+委托层)+r3-3(单源 const+写侧/两读侧改派生)+键集断言(wr2)。可核查结果:level_runner ≈780 读数记录;27 键对照表逐键一致;断点域套件+iter36 wc2 断言绿。 +- [ ] 验收步骤:全套闸按域选档跑一次全绿。 + +### T2 令牌池拆件(≈0.75d) + +- [ ] 实装 r3-1(curio_deck 拆出+委托层+停点面留宿主边界落地)。可核查结果:adventure_runner ≈850±10 读数记录;逐字迁移核对表逐函数落位;令牌/商店域全部既有套件绿。 +- [ ] 验收步骤:全套闸全绿+改前改后读数对拍。 + +### T3 撤销证明+产出物落位卡(≈0.25d,末卡) + +- [ ] 撤销证明两条(P0 令形态——只回退本批 hunk):①回退 shop_session 承接件任一函数 hunk(如 `pool_price` 承接体)→ 宿主委托/消费面引用未定义符号 → 套件直跑经 `tools/error_scan.py` 扫出 SCRIPT ERROR 红 → 还原复绿;②回退单源 const hunk → 键集断言直跑 error_scan 红(常量未定义)→ 还原复绿。记录进实现报告。 +- [ ] 写齐三份产出物(实现报告含两核对表+行数读数+27 键对照表+实机目检总清单〔商店购买/刷新/三选一/续玩断点抽查〕/代拍板追认单/反哺点)。 +- [ ] 合并前置:独立验收审计(同卡①口径——抽查重点=27 键对照表与逐字迁移核对表)逐行「通过」。 + +**估时合计 ≈2–2.5d**。编排注记:前置卡①;与 R1/R2 可同迭代分波;断点面重(键集断言+五时点+iter36 wc2 回归)。 + +## 8. 验证闸 + +### 改动—验收对照表 + +| 需求点 | 改动 | 验收测试(尾缀) | 层级 | 真实入口 | 撤销证明 | +|---|---|---|---|---|---| +| wr1 三单元等价 | 两 runner 拆件+断点单源 | 全套闸绿+两核对表+27 键对照表〔wr1〕 | 集成 | 断点/商店/令牌域真实套件+系统测试+ui_click_probe(iter36 wc2) | 撤销证明两条(T3);等价三证替代逐单元撤销(等价重构申报) | +| wr2 断点键集断言 | breakpoint_resume_test 增一条 | 单源键集==写侧字典键集〔wr2〕 | 单元 | `_snapshot_breakpoint` 真实拼装路径 | 回退单源 const hunk → error_scan 红(T3②) | +| wr3 新件合规 | 两新件+单源 | S1-S5/S9/S10/S11/S13 全零命中〔wr3〕 | 静态扫描 | 闸真实跑批 | 回退任一合规面 → 对应闸红 | +| wr4 产出物 | T3 三份 | 件核对〔wr4〕 | — | — | 缺项红 | + +**测试削弱申报**:零(既有断言零改动;增断言一条)。**独立验收审计**:同卡① §8.3 口径。 + +## 9. 异常分级 + +- **L1(自救)**:两新件命名;委托层薄厚;单源 const 形态与策略标签命名;宿主记账回调具体形态。 +- **L2(记录继续)**:A5 直引清单个别适配;行数读数与预期的正常偏差。 +- **L3(停机呈报)**:迁移体无法逐字等价(耦合面比 A1/A2 判定宽——判离指认失真回呈);断点对照表任一键不一致;任一断点/商店/令牌域套件或 iter36 wc2 断言非本批存量红;单源化需动 save_manager 结构(超 r3-3 范围);r3-1 边界函数在实装中被迫跨侧(停点面与池面无法分离)。 + +## 10. 代拍板白名单 + +1. 两新件命名与去下划线取舍(签名语义不变前提)。 +2. 宿主记账回调的具体形态。 +3. `spend_gp` 随迁的回调落账写法。 +4. 提交切分与 worktree 分支命名。 + +## 11. 待裁点 + +**零待裁点,开工门=U0。** 三处灰色地带推导定值(呈报可否决,随 T3 反哺点):①断点单源落**宿主内 const** 而非独立件——依据=27 键多数取宿主态、独立件开洞面大(预检节);②**拆池不拆停点**的边界(refresh 族/兑付推进族留宿主)——依据=停点态与断点五时点强耦合宿主流程,全拆=行为面扩大违零变更契约(grilling R3-A1 订正 v1.0 全族迁移方案);③`spend_gp` 随商店件迁、经回调落宿主账本——依据=它只服务商店扣款而账本原语单点在宿主。备选淘汰依据各见预检/契约。 + +## 12. 自审六查与 grilling 复核记录 + +**自审六查(2026-10-24 写卡会话)**:①规格覆盖——r3-1~r3-3→T1/T2、断言/合规/撤销/产出物→T1/T3,指到 wr1–wr4;②步骤扫描——一步一动作一可核查结果;③类型一致性——接口块与 A1-A6 实查一致(v1.1 边界重划后逐函数归属两侧清单化);④审查焦点——断点 27 键对照表(契约 1/3)、键集断言(wr2)、五时点全留宿主(契约 3)、A6 交点、iter36 wc2 回归纳入;⑤篇幅比例——零实现体;⑥落位预检——两新件唯一性、单源随宿主、账本留宿主边界均已答。 + +**grilling 第一轮复核记录(2026-10-25,独立子代理对抗复核)**:判回炉两阻断——R3-A1 r3-1 清单圈入 refresh 族/兑付面与「拆池不拆停点」边界直接互斥(断点时点 :634/:803 住在 refresh/兑付函数体内,随迁即扰动五时点;行数账两头堵);R3-A2 T3 两条变异违 2026-10-24 P0 令。随同五应修(F1 断点实为 27 顶层键非 28;F2 读侧回退语义远超 {default,deep} 双字段——五类策略实语义;F3 读侧漏 `_start_current_level` 四键二次消费;F4 save 侧零键登记、「三处」论断不成立;F5 iter36 wc2 断点断言未纳入+`_consume_deck_token` 漏迁+两函数归属未交代)+两建议(A2 区间收窄;set_pool 缺省值对齐)。v1.1 全部折入:r3-1 边界重划(迁出/留宿主两侧函数清单化、五时点全留宿主、`_consume_deck_token` 补入、`_feat_payload_keys`/`_apply_curio_pick` 归属留宿主)、27 键订正、读侧两消费位+五类策略登记、save 侧空派生订正(§12 回呈审计)、证明改撤销形态两条、iter36 wc2 纳入 T0 基线与回归面、区间收窄为函数清单、set_pool 逐字对齐。复核结论:两阻断闭合、五应修落位(v1.1)。 + +**grilling 第二轮复核记录(2026-10-25,同代理续核)**:总判**通过(附应修)**——A1 边界重划真闭合(两侧清单逐项对账无重叠无遗漏、五时点全留宿主实锤)、A2 撤销证明合规、F1–F5 全落。新发现两应修:①行数读数 710–730 系第一轮旧口径未随边界扩面重算(按 v1.1 清单实算迁出 ≈211 行→收 ≈850±10);②契约 5「其余面不动」与 :883-884 接线行互斥(四标识符全在迁出面,不改该行无法编译)。v1.2 折入:读数改 ≈850±10、契约 5 增接线行例外并列入 r3-1 改动面、策略词表统一六类、feats_held 留驻一行明示。零待裁点成立,卡进可编排态。 diff --git a/roadmap.md b/roadmap.md index cbeac4ec..aa815898 100644 --- a/roadmap.md +++ b/roadmap.md @@ -5,8 +5,8 @@ ## 当前位(只回答「现在该做什么」;三行内随批次换代,禁止在此扩写成状态快照) - **阶段**:M3——Alpha 版本的完整实现(2026-10-09 立规:未明令宣布完成前停留 M3,迭代只排 M3 需求,M4 不排期;根目录 stage.txt 为旧阶段开关仅存档)。 -- **迭代位**:迭代 1–33 全部已完成或已终止——迭代 33 三卡合回并于 2026-10-22 总收口+反哺落文,迭代 34 两卡已合回、批后收尾未做;迭代 35 已编排(待开工,卡片在 reports/);迭代 36 起未编排——编入哪一迭代由用户经迭代编排段发令。 -- **下一步**:迭代 34 批后收尾(收口登记与遗留读数义务清册,由用户发令)→ `/iter 35` 发令开工(实现段,段间不自动流转)。待编池:营地交互重做卡⑤(排卡④之后)、卡④(已退回美术设计段,美术设计闭合后重立卡)、冒险前资源配比预设(冻结备选)、迭代 30 原卡②③(2026-10-17 已剔出迭代 30 规划、退回待设计,待美术/设计定稿后重新立卡)、玩家面残留禁字清扫(待写卡)、结算明细日志行序重锚(2026-10-21 授权发起、时点另定)。 +- **迭代位**:迭代 1–35 全部已完成——迭代 34 于 2026-10-24 完成批后收尾;迭代 35 两卡于 2026-10-25 合回并完成批后收尾;迭代 36 三卡已合回;迭代 37 起未编排——编入哪一迭代由用户经迭代编排段发令。 +- **下一步**:迭代 36 补记迭代总表行(现仍写「待开工」,与实际不符);迭代 37 编排由用户发令(迭代编排段入口)。待编池:营地交互重做卡⑤(排卡④之后)、卡④(已退回美术设计段,美术设计闭合后重立卡)、冒险前资源配比预设(冻结备选)、迭代 30 原卡②③(2026-10-17 已剔出迭代 30 规划、退回待设计,待美术/设计定稿后重新立卡)、玩家面残留禁字清扫(待写卡)、结算明细日志行序重锚(2026-10-21 授权发起、时点另定)。 ## 里程碑时间线 @@ -67,8 +67,8 @@ | 迭代 31 | 已完成(2026-10-19) | 已退役(git 历史可考) | 2026-10-18 | ≈7.5 个工作日 | 已退役(git 历史可考) | | 迭代 32 | 已完成(2026-10-21) | 已退役(git 历史可考) | 2026-10-20 | ≈8.75 个工作日 | 已退役(git 历史可考) | | 迭代 33 | 已完成(2026-10-22) | 已退役(git 历史可考) | 2026-10-21 | ≈13.25 个工作日〔用户特批 2026-10-21 超 10 个工作日〕 | 已退役(git 历史可考) | -| 迭代 34 | 待开工 | reports/需求卡片-敌人表深度带换代批-2026-10-21.md
reports/需求卡片-信息迷雾可见性批-2026-10-21.md | 2026-10-21 | ≈11 个工作日〔用户特批 2026-10-21 超 10 个工作日〕 | — | -| 迭代 35 | 待开工 | reports/需求卡片-节点图生成器核心批-2026-10-21.md
reports/需求卡片-节点图消费侧换代批-2026-10-21.md | 2026-10-21 | ≈15.25 个工作日〔用户特批 2026-10-21 超 10 个工作日〕 | — | +| 迭代 34 | 已完成(2026-10-22) | 已退役(git 历史可考) | 2026-10-21 | ≈11 个工作日〔用户特批 2026-10-21 超 10 个工作日〕 | 已退役(git 历史可考) | +| 迭代 35 | 已完成(2026-10-25) | 已退役(git 历史可考) | 2026-10-21 | ≈15.25 个工作日〔用户特批 2026-10-21 超 10 个工作日〕 | 已退役(git 历史可考) | | 迭代 36 | 待开工 | reports/需求卡片-开场后缀营地灰字迷雾测点批-2026-10-24.md
reports/需求卡片-文案与禁字清扫批-2026-10-24.md
reports/需求卡片-状态面板瘦身批-2026-10-23.md | 2026-10-24 | ≈5.25 个工作日 | — | - 历史行的「—」= 该列事实不可考:迭代 1~8 早于需求卡片制;编排时点与预计周期不再回溯,细节查 git 提交史。 diff --git a/tools/gdstyle.toml b/tools/gdstyle.toml new file mode 100644 index 00000000..4f097cd7 --- /dev/null +++ b/tools/gdstyle.toml @@ -0,0 +1,52 @@ +# gdstyle.toml — S13 第三方质量规则闸的 gdstyle 配置(tools/gdstyle_gate.py 以 -c 显式指定本文件) +# +# 只开质量类规则:抓真缺陷(重复 load/循环内 await/循环内分配/不可达代码/自比较/字典重复键等), +# 存量进 tools/gdstyle_baseline.txt,基线外新违规才红。format/*/naming/*/order/* 与 S9/S10 职能重叠 +# 全关(gdstyle 不支持通配符关停,须逐条枚举);syntax/lex-error 关——core/rng_service.gd 的 +# INT64_MIN 负字面量是已知误报(Godot 自家解析器放行);type-hint 与 S9 类型闸重叠关; +# empty-function 关(合法桩形态自由)。quality/no-debug-print 本是 opt-in 默认不跑,此处显式开 +# ——产品码残留 print() 属真缺陷。阈值(函数长度/参数数等)取 gdstyle 默认值: +# 改阈值=基线口径变,须随批人工重生成基线。工具版本钉 v0.4.0(闸件校验,升级须重测兼容性)。 + +[rules] +# ── 格式类全关(与 S9 类型闸/S10 文件头闸职能重叠)── +"format/blank-lines" = "off" +"format/boolean-operators" = "off" +"format/brace-spacing" = "off" +"format/call-paren-spacing" = "off" +"format/colon-spacing" = "off" +"format/comma-spacing" = "off" +"format/comment-spacing" = "off" +"format/double-quotes" = "off" +"format/enum-one-per-line" = "off" +"format/float-literal-zeros" = "off" +"format/large-number-underscores" = "off" +"format/max-line-length" = "off" +"format/no-tabs-as-spaces" = "off" +"format/no-unnecessary-parens" = "off" +"format/number-literals" = "off" +"format/one-statement-per-line" = "off" +"format/operator-spacing" = "off" +"format/trailing-comma" = "off" +"format/trailing-newline" = "off" +"format/trailing-whitespace" = "off" +# ── 命名类全关(命名纪律归 code-conventions 与评审面)── +"naming/class-name-pascal-case" = "off" +"naming/constant-name-screaming-case" = "off" +"naming/enum-member-screaming-case" = "off" +"naming/enum-name-pascal-case" = "off" +"naming/file-name-snake-case" = "off" +"naming/function-name-snake-case" = "off" +"naming/node-name-pascal-case" = "off" +"naming/private-underscore-prefix" = "off" +"naming/signal-name-snake-case" = "off" +"naming/signal-past-tense" = "off" +"naming/variable-name-snake-case" = "off" +# ── 排序类关(官方风格指南性质,存量过大)── +"order/class-member-order" = "off" +# ── 语法类:lex-error 关(已知误报),reserved-identifier 留守(保留字当名字=真缺陷)── +"syntax/lex-error" = "off" +# ── 质量类关两条(职能重叠),其余 quality/* 全部默认 warn 在跑 ── +"quality/type-hint" = "off" +"quality/empty-function" = "off" +"quality/no-debug-print" = "warn" diff --git a/tools/gdstyle_baseline.txt b/tools/gdstyle_baseline.txt new file mode 100644 index 00000000..04fb2633 --- /dev/null +++ b/tools/gdstyle_baseline.txt @@ -0,0 +1,241 @@ +# gdstyle 存量基线(S13)——文件|规则|消息 三元组,每行一条;只记基线不记豁免理由。 +# 重生成=人工专用:python tools/gdstyle_gate.py --refresh(禁清基线换绿;改规则口径/工具升级须随批重生成)。 +game/core/action_economy.gd|quality/max-class-variables|class has 18 variables (max 15) +game/core/adventure_runner.gd|quality/max-branches|function '_apply_curio_pick' has 10 branches (max 8) +game/core/adventure_runner.gd|quality/max-branches|function '_draw_curio_candidates' has 11 branches (max 8) +game/core/adventure_runner.gd|quality/max-branches|function 'resume_adventure' has 10 branches (max 8) +game/core/adventure_runner.gd|quality/max-class-variables|class has 43 variables (max 15) +game/core/adventure_runner.gd|quality/max-file-length|file is 1050 lines long (max 1000) +game/core/adventure_runner.gd|quality/max-function-length|function '_draw_curio_candidates' is 85 lines long (max 50) +game/core/adventure_runner.gd|quality/max-function-length|function '_start_current_level' is 55 lines long (max 50) +game/core/adventure_runner.gd|quality/max-function-length|function 'resume_adventure' is 104 lines long (max 50) +game/core/adventure_runner.gd|quality/max-local-variables|function '_draw_curio_candidates' has 26 local variables (max 10) +game/core/adventure_runner.gd|quality/max-local-variables|function 'resume_adventure' has 13 local variables (max 10) +game/core/adventure_runner.gd|quality/max-public-methods|class has 23 public methods (max 20) +game/core/adventure_runner.gd|quality/max-returns|function '_apply_curio_pick' has 9 return statements (max 6) +game/core/battle_orchestrator.gd|quality/max-branches|function 'begin_battle' has 17 branches (max 8) +game/core/battle_orchestrator.gd|quality/max-branches|function 'use_treasure' has 18 branches (max 8) +game/core/battle_orchestrator.gd|quality/max-class-variables|class has 50 variables (max 15) +game/core/battle_orchestrator.gd|quality/max-file-length|file is 1302 lines long (max 1000) +game/core/battle_orchestrator.gd|quality/max-function-length|function '_place_variants' is 52 lines long (max 50) +game/core/battle_orchestrator.gd|quality/max-function-length|function 'begin_battle' is 141 lines long (max 50) +game/core/battle_orchestrator.gd|quality/max-function-length|function 'use_treasure' is 62 lines long (max 50) +game/core/battle_orchestrator.gd|quality/max-local-variables|function '_place_variants' has 14 local variables (max 10) +game/core/battle_orchestrator.gd|quality/max-local-variables|function 'begin_battle' has 16 local variables (max 10) +game/core/battle_orchestrator.gd|quality/max-local-variables|function 'use_treasure' has 11 local variables (max 10) +game/core/battle_orchestrator.gd|quality/max-nesting-depth|function '_place_variants' has nesting depth 6 (max 4) +game/core/battle_orchestrator.gd|quality/max-parameters|function '_apply_flat_row' has 7 parameters (max 5) +game/core/battle_orchestrator.gd|quality/max-parameters|function '_apply_gp_row' has 6 parameters (max 5) +game/core/battle_orchestrator.gd|quality/max-parameters|function '_apply_percent_row' has 6 parameters (max 5) +game/core/battle_orchestrator.gd|quality/max-parameters|function '_dispatch_row' has 6 parameters (max 5) +game/core/battle_orchestrator.gd|quality/max-parameters|function '_emit_row' has 8 parameters (max 5) +game/core/battle_orchestrator.gd|quality/max-parameters|function '_grant_routed' has 8 parameters (max 5) +game/core/battle_orchestrator.gd|quality/max-parameters|function '_submit_flat_hero' has 7 parameters (max 5) +game/core/battle_orchestrator.gd|quality/max-returns|function '_apply_flat_row' has 9 return statements (max 6) +game/core/battle_orchestrator.gd|quality/max-returns|function '_fx_fact' has 7 return statements (max 6) +game/core/battle_orchestrator.gd|quality/max-returns|function 'begin_battle' has 7 return statements (max 6) +game/core/battle_orchestrator.gd|quality/max-returns|function 'use_treasure' has 8 return statements (max 6) +game/core/board_generator.gd|quality/max-branches|function 'measure_inferability' has 12 branches (max 8) +game/core/board_generator.gd|quality/max-function-length|function '_attempt' is 51 lines long (max 50) +game/core/board_generator.gd|quality/max-function-length|function 'generate' is 62 lines long (max 50) +game/core/board_generator.gd|quality/max-function-length|function 'measure_inferability' is 64 lines long (max 50) +game/core/board_generator.gd|quality/max-local-variables|function 'generate' has 14 local variables (max 10) +game/core/board_generator.gd|quality/max-local-variables|function 'measure_inferability' has 13 local variables (max 10) +game/core/board_generator.gd|quality/max-nesting-depth|function 'measure_inferability' has nesting depth 5 (max 4) +game/core/board_generator.gd|quality/max-parameters|function '_finalize' has 6 parameters (max 5) +game/core/config_db.gd|quality/max-class-variables|class has 25 variables (max 15) +game/core/config_db.gd|quality/max-public-methods|class has 44 public methods (max 20) +game/core/config_db_cross_validator.gd|quality/max-branches|function 'cross_validate' has 31 branches (max 8) +game/core/config_db_cross_validator.gd|quality/max-function-length|function 'cross_validate' is 159 lines long (max 50) +game/core/config_db_cross_validator.gd|quality/max-local-variables|function 'cross_validate' has 39 local variables (max 10) +game/core/config_db_cross_validator.gd|quality/max-local-variables|function 'validate_skill_pool_links' has 11 local variables (max 10) +game/core/config_db_cross_validator.gd|quality/max-nesting-depth|function 'cross_validate' has nesting depth 6 (max 4) +game/core/config_db_resolver.gd|quality/max-branches|function '_resolve_progress' has 16 branches (max 8) +game/core/config_db_resolver.gd|quality/max-branches|function 'resolve_enemy_rowset' has 12 branches (max 8) +game/core/config_db_resolver.gd|quality/max-function-length|function '_resolve_progress' is 52 lines long (max 50) +game/core/config_db_resolver.gd|quality/max-local-variables|function '_resolve_progress' has 11 local variables (max 10) +game/core/damage_controller.gd|quality/max-branches|function '_hero_attack_ctx' has 9 branches (max 8) +game/core/damage_controller.gd|quality/max-branches|function '_hero_attack_redeem' has 10 branches (max 8) +game/core/damage_controller.gd|quality/max-class-variables|class has 19 variables (max 15) +game/core/damage_controller.gd|quality/max-function-length|function '_hero_attack_ctx' is 61 lines long (max 50) +game/core/damage_controller.gd|quality/max-function-length|function '_hero_attack_redeem' is 54 lines long (max 50) +game/core/damage_controller.gd|quality/max-local-variables|function '_hero_attack_ctx' has 16 local variables (max 10) +game/core/damage_controller.gd|quality/max-local-variables|function '_hero_attack_redeem' has 12 local variables (max 10) +game/core/damage_controller.gd|quality/max-local-variables|function '_resolve_enemy_skill' has 13 local variables (max 10) +game/core/damage_controller.gd|quality/max-parameters|function 'open_ticket' has 12 parameters (max 5) +game/core/effect_engine.gd|quality/max-branches|function 'validate_trigger_row' has 14 branches (max 8) +game/core/effect_engine.gd|quality/max-function-length|function '_cond_ok' is 52 lines long (max 50) +game/core/effect_engine.gd|quality/max-function-length|function 'rebuild' is 55 lines long (max 50) +game/core/effect_engine.gd|quality/max-local-variables|function 'validate_trigger_row' has 11 local variables (max 10) +game/core/effect_engine.gd|quality/max-nesting-depth|function '_cond_ok' has nesting depth 5 (max 4) +game/core/effect_engine.gd|quality/max-nesting-depth|function 'rebuild' has nesting depth 5 (max 4) +game/core/effect_engine.gd|quality/max-returns|function '_cond_ok' has 19 return statements (max 6) +game/core/effect_engine.gd|quality/no-else-return|unnecessary 'elif' after 'return' +game/core/level_runner.gd|quality/max-class-variables|class has 40 variables (max 15) +game/core/level_runner.gd|quality/max-file-length|file is 1013 lines long (max 1000) +game/core/level_runner.gd|quality/max-parameters|function 'start_level_external' has 8 parameters (max 5) +game/core/level_runner.gd|quality/max-public-methods|class has 54 public methods (max 20) +game/core/level_runner.gd|quality/max-returns|function '_enter_node' has 7 return statements (max 6) +game/core/meta_progression.gd|quality/max-public-methods|class has 31 public methods (max 20) +game/core/meta_progression.gd|quality/max-returns|function 'tree_can_buy' has 8 return statements (max 6) +game/core/minesweeper_rules.gd|quality/max-function-length|function '_do_dig' is 53 lines long (max 50) +game/core/node_map_generator.gd|quality/max-branches|function '_assign_types' has 11 branches (max 8) +game/core/node_map_generator.gd|quality/max-branches|function 'check_params' has 12 branches (max 8) +game/core/node_map_generator.gd|quality/max-branches|function 'validate_output' has 34 branches (max 8) +game/core/node_map_generator.gd|quality/max-function-length|function 'validate_output' is 132 lines long (max 50) +game/core/node_map_generator.gd|quality/max-local-variables|function '_add_forks' has 12 local variables (max 10) +game/core/node_map_generator.gd|quality/max-local-variables|function '_build' has 15 local variables (max 10) +game/core/node_map_generator.gd|quality/max-local-variables|function 'validate_output' has 22 local variables (max 10) +game/core/node_map_generator.gd|quality/max-nesting-depth|function '_check_fork_rules' has nesting depth 5 (max 4) +game/core/node_map_generator.gd|quality/max-returns|function 'validate_output' has 7 return statements (max 6) +game/core/reward_economy.gd|quality/max-function-length|function 'roll_board' is 54 lines long (max 50) +game/core/reward_economy.gd|quality/max-local-variables|function '_skewed_cell_rows' has 13 local variables (max 10) +game/core/reward_economy.gd|quality/max-local-variables|function 'roll_board' has 11 local variables (max 10) +game/core/save_manager.gd|quality/max-public-methods|class has 52 public methods (max 20) +game/core/save_manager.gd|quality/max-returns|function '_peek_summary_at' has 7 return statements (max 6) +game/core/save_manager.gd|quality/max-returns|function 'load_from_disk' has 8 return statements (max 6) +game/core/settlement/combat_segment.gd|quality/max-function-length|function 'run' is 83 lines long (max 50) +game/core/settlement/combat_segment.gd|quality/max-local-variables|function 'run' has 13 local variables (max 10) +game/core/settlement/grant_segment.gd|quality/max-branches|function '_judge_seal' has 14 branches (max 8) +game/core/settlement/grant_segment.gd|quality/max-branches|function 'run' has 33 branches (max 8) +game/core/settlement/grant_segment.gd|quality/max-function-length|function '_judge_seal' is 84 lines long (max 50) +game/core/settlement/grant_segment.gd|quality/max-function-length|function 'run' is 211 lines long (max 50) +game/core/settlement/grant_segment.gd|quality/max-local-variables|function '_judge_seal' has 11 local variables (max 10) +game/core/settlement/grant_segment.gd|quality/max-local-variables|function 'run' has 36 local variables (max 10) +game/core/settlement/grant_segment.gd|quality/max-nesting-depth|function '_judge_seal' has nesting depth 6 (max 4) +game/core/settlement/grant_segment.gd|quality/max-nesting-depth|function 'run' has nesting depth 6 (max 4) +game/core/status_container.gd|quality/max-nesting-depth|function 'apply_effect' has nesting depth 5 (max 4) +game/core/status_container.gd|quality/max-parameters|function '_new_instance' has 7 parameters (max 5) +game/core/status_container.gd|quality/max-returns|function 'apply_effect' has 9 return statements (max 6) +game/core/treasure_items.gd|quality/max-branches|function 'validate_use' has 11 branches (max 8) +game/core/treasure_items.gd|quality/max-nesting-depth|function 'validate_use' has nesting depth 5 (max 4) +game/core/unit_container.gd|quality/max-function-length|function 'spawn' is 81 lines long (max 50) +game/core/unit_container.gd|quality/max-local-variables|function 'spawn' has 17 local variables (max 10) +game/core/unit_container.gd|quality/max-parameters|function 'spawn' has 10 parameters (max 5) +game/data/schemas/adventure_table.gd|quality/max-class-variables|class has 44 variables (max 15) +game/data/schemas/board_gen_table.gd|quality/max-branches|function 'validate' has 20 branches (max 8) +game/data/schemas/board_gen_table.gd|quality/max-function-length|function 'validate' is 67 lines long (max 50) +game/data/schemas/board_gen_table.gd|quality/max-local-variables|function 'validate' has 16 local variables (max 10) +game/data/schemas/enemy_table.gd|quality/max-branches|function 'validate' has 32 branches (max 8) +game/data/schemas/enemy_table.gd|quality/max-function-length|function 'validate' is 116 lines long (max 50) +game/data/schemas/enemy_table.gd|quality/max-local-variables|function 'validate' has 29 local variables (max 10) +game/data/schemas/forge_table.gd|quality/max-branches|function 'validate' has 30 branches (max 8) +game/data/schemas/forge_table.gd|quality/max-function-length|function 'validate' is 95 lines long (max 50) +game/data/schemas/forge_table.gd|quality/max-local-variables|function 'validate' has 20 local variables (max 10) +game/data/schemas/level_gen_params.gd|quality/max-branches|function 'validate' has 15 branches (max 8) +game/data/schemas/level_gen_params.gd|quality/max-function-length|function 'validate' is 59 lines long (max 50) +game/data/schemas/level_gen_params.gd|quality/max-local-variables|function 'validate' has 18 local variables (max 10) +game/data/schemas/level_table.gd|quality/max-branches|function '_validate_graph' has 21 branches (max 8) +game/data/schemas/level_table.gd|quality/max-branches|function '_validate_level_nodes' has 12 branches (max 8) +game/data/schemas/level_table.gd|quality/max-function-length|function '_validate_graph' is 99 lines long (max 50) +game/data/schemas/level_table.gd|quality/max-local-variables|function '_validate_graph' has 19 local variables (max 10) +game/data/schemas/meta_table.gd|quality/max-branches|function 'validate' has 14 branches (max 8) +game/data/schemas/meta_table.gd|quality/max-function-length|function 'validate' is 60 lines long (max 50) +game/data/schemas/meta_table.gd|quality/max-local-variables|function 'validate' has 14 local variables (max 10) +game/data/schemas/meta_tree_table.gd|quality/max-branches|function 'validate' has 26 branches (max 8) +game/data/schemas/meta_tree_table.gd|quality/max-function-length|function 'validate' is 86 lines long (max 50) +game/data/schemas/meta_tree_table.gd|quality/max-local-variables|function 'validate' has 13 local variables (max 10) +game/data/schemas/meta_tree_table.gd|quality/max-nesting-depth|function 'validate' has nesting depth 5 (max 4) +game/data/schemas/reward_table.gd|quality/max-branches|function 'validate' has 30 branches (max 8) +game/data/schemas/reward_table.gd|quality/max-function-length|function 'validate' is 99 lines long (max 50) +game/data/schemas/reward_table.gd|quality/max-local-variables|function 'validate' has 20 local variables (max 10) +game/data/schemas/settlement_table.gd|quality/max-branches|function 'validate' has 16 branches (max 8) +game/data/schemas/settlement_table.gd|quality/max-class-variables|class has 18 variables (max 15) +game/data/schemas/shop_table.gd|quality/max-branches|function 'validate' has 22 branches (max 8) +game/data/schemas/shop_table.gd|quality/max-function-length|function 'validate' is 84 lines long (max 50) +game/data/schemas/shop_table.gd|quality/max-local-variables|function 'validate' has 20 local variables (max 10) +game/data/schemas/skill_table.gd|quality/max-branches|function 'validate' has 29 branches (max 8) +game/data/schemas/skill_table.gd|quality/max-function-length|function 'validate' is 80 lines long (max 50) +game/data/schemas/status_effect_table.gd|quality/max-branches|function 'validate' has 12 branches (max 8) +game/data/schemas/treasure_item_table.gd|quality/max-branches|function 'validate' has 44 branches (max 8) +game/data/schemas/treasure_item_table.gd|quality/max-function-length|function 'validate' is 147 lines long (max 50) +game/data/schemas/treasure_item_table.gd|quality/max-local-variables|function 'validate' has 26 local variables (max 10) +game/data/schemas/treasure_item_table.gd|quality/max-nesting-depth|function 'validate' has nesting depth 6 (max 4) +game/data/schemas/variation_table.gd|quality/max-branches|function 'validate' has 20 branches (max 8) +game/data/schemas/variation_table.gd|quality/max-function-length|function 'validate' is 58 lines long (max 50) +game/presentation/battle_narrator.gd|quality/max-returns|function 'bonus_card_text' has 9 return statements (max 6) +game/presentation/camp_flow_context.gd|quality/max-parameters|function '_init' has 12 parameters (max 5) +game/presentation/curio_flow_context.gd|quality/max-class-variables|class has 16 variables (max 15) +game/presentation/curio_flow_context.gd|quality/max-parameters|function '_init' has 16 parameters (max 5) +game/presentation/curio_interact_controller.gd|quality/max-class-variables|class has 18 variables (max 15) +game/presentation/hud_presenter.gd|quality/max-parameters|function '_refresh_out_of_battle' has 6 parameters (max 5) +game/presentation/hud_presenter.gd|quality/max-parameters|function 'refresh' has 8 parameters (max 5) +game/presentation/hud_presenter.gd|quality/max-parameters|function 'tune_sim_report' has 10 parameters (max 5) +game/presentation/main.gd|quality/max-class-variables|class has 49 variables (max 15) +game/presentation/main.gd|quality/max-file-length|file is 1509 lines long (max 1000) +game/presentation/main.gd|quality/max-function-length|function '_build_ui' is 189 lines long (max 50) +game/presentation/playtest_logger.gd|quality/no-debug-print|debug 'print()' call found +game/presentation/treasure_interact_controller.gd|quality/max-local-variables|function 'inventory_projection' has 11 local variables (max 10) +game/presentation/treasure_interact_controller.gd|quality/max-parameters|function '_init' has 7 parameters (max 5) +game/presentation/tune_sim_controller.gd|quality/await-in-loop|'await' used inside a loop +game/presentation/tune_sim_controller.gd|quality/max-local-variables|function 'run' has 11 local variables (max 10) +game/presentation/tune_sim_controller.gd|quality/no-debug-print|debug 'print()' call found +game/presentation/ui_assembler.gd|quality/allocation-in-loop|object allocation '.new()' inside a loop +game/presentation/ui_assembler.gd|quality/max-function-length|function 'build_start_overlay' is 83 lines long (max 50) +game/presentation/ui_assembler.gd|quality/max-local-variables|function 'build_start_overlay' has 16 local variables (max 10) +game/presentation/views/ap_bar_view.gd|quality/max-function-length|function '_init' is 69 lines long (max 50) +game/presentation/views/battle_end_overlay.gd|quality/allocation-in-loop|object allocation '.new()' inside a loop +game/presentation/views/battle_end_overlay.gd|quality/max-class-variables|class has 20 variables (max 15) +game/presentation/views/battle_end_overlay.gd|quality/max-function-length|function '_init' is 81 lines long (max 50) +game/presentation/views/battle_end_overlay.gd|quality/max-parameters|function 'open_victory' has 8 parameters (max 5) +game/presentation/views/battle_log_view.gd|quality/max-branches|function '_format_row' has 9 branches (max 8) +game/presentation/views/battle_log_view.gd|quality/max-function-length|function '_format_row' is 68 lines long (max 50) +game/presentation/views/battle_log_view.gd|quality/max-local-variables|function '_format_row' has 21 local variables (max 10) +game/presentation/views/battle_log_view.gd|quality/max-returns|function '_format_row' has 13 return statements (max 6) +game/presentation/views/battle_stage_view.gd|quality/allocation-in-loop|object allocation '.new()' inside a loop +game/presentation/views/battle_stage_view.gd|quality/max-function-length|function '_init' is 97 lines long (max 50) +game/presentation/views/battle_stage_view.gd|quality/max-returns|function '_enemy_portrait_tex' has 9 return statements (max 6) +game/presentation/views/board_cell_unit.gd|quality/max-branches|function '_draw' has 29 branches (max 8) +game/presentation/views/board_cell_unit.gd|quality/max-class-variables|class has 31 variables (max 15) +game/presentation/views/board_cell_unit.gd|quality/max-function-length|function '_draw' is 147 lines long (max 50) +game/presentation/views/board_cell_unit.gd|quality/max-local-variables|function '_draw' has 19 local variables (max 10) +game/presentation/views/board_fx_view.gd|quality/allocation-in-loop|object allocation '.new()' inside a loop +game/presentation/views/board_view.gd|quality/allocation-in-loop|object allocation '.new()' inside a loop +game/presentation/views/board_view.gd|quality/max-branches|function '_paint_cell' has 24 branches (max 8) +game/presentation/views/board_view.gd|quality/max-branches|function '_wall_tile_for' has 14 branches (max 8) +game/presentation/views/board_view.gd|quality/max-class-variables|class has 45 variables (max 15) +game/presentation/views/board_view.gd|quality/max-file-length|file is 1220 lines long (max 1000) +game/presentation/views/board_view.gd|quality/max-function-length|function '_paint_cell' is 139 lines long (max 50) +game/presentation/views/board_view.gd|quality/max-local-variables|function '_digit_ink_mode' has 12 local variables (max 10) +game/presentation/views/board_view.gd|quality/max-local-variables|function '_paint_cell' has 22 local variables (max 10) +game/presentation/views/board_view.gd|quality/max-public-methods|class has 27 public methods (max 20) +game/presentation/views/board_view.gd|quality/max-returns|function '_wall_center_tile' has 7 return statements (max 6) +game/presentation/views/board_view.gd|quality/max-returns|function '_wall_tile_for' has 15 return statements (max 6) +game/presentation/views/board_zone_view.gd|quality/max-function-length|function '_init' is 59 lines long (max 50) +game/presentation/views/camp_overlay_view.gd|quality/allocation-in-loop|object allocation '.new()' inside a loop +game/presentation/views/camp_overlay_view.gd|quality/max-class-variables|class has 19 variables (max 15) +game/presentation/views/camp_overlay_view.gd|quality/max-function-length|function '_init' is 149 lines long (max 50) +game/presentation/views/camp_overlay_view.gd|quality/max-function-length|function '_rebuild_forge' is 72 lines long (max 50) +game/presentation/views/camp_overlay_view.gd|quality/max-function-length|function '_rebuild_tree' is 61 lines long (max 50) +game/presentation/views/camp_overlay_view.gd|quality/max-function-length|function 'refresh_camp' is 61 lines long (max 50) +game/presentation/views/camp_overlay_view.gd|quality/max-local-variables|function '_init' has 28 local variables (max 10) +game/presentation/views/camp_overlay_view.gd|quality/max-local-variables|function '_rebuild_forge' has 20 local variables (max 10) +game/presentation/views/camp_overlay_view.gd|quality/max-local-variables|function '_rebuild_tree' has 21 local variables (max 10) +game/presentation/views/camp_overlay_view.gd|quality/max-local-variables|function 'refresh_camp' has 17 local variables (max 10) +game/presentation/views/curio_pick_overlay.gd|quality/max-function-length|function '_init' is 52 lines long (max 50) +game/presentation/views/curio_pick_overlay.gd|quality/max-returns|function '_icon_for_id' has 7 return statements (max 6) +game/presentation/views/debug_overlay.gd|quality/max-class-variables|class has 17 variables (max 15) +game/presentation/views/debug_overlay.gd|quality/max-function-length|function '_build' is 124 lines long (max 50) +game/presentation/views/debug_overlay.gd|quality/max-local-variables|function '_build' has 22 local variables (max 10) +game/presentation/views/hero_info_panel_view.gd|quality/allocation-in-loop|object allocation '.new()' inside a loop +game/presentation/views/hero_info_panel_view.gd|quality/max-function-length|function '_init' is 74 lines long (max 50) +game/presentation/views/hero_info_panel_view.gd|quality/max-function-length|function 'set_treasure_inventory' is 57 lines long (max 50) +game/presentation/views/hero_info_panel_view.gd|quality/max-local-variables|function 'set_treasure_inventory' has 13 local variables (max 10) +game/presentation/views/legend_overlay.gd|quality/allocation-in-loop|object allocation '.new()' inside a loop +game/presentation/views/level_map_screen.gd|quality/max-branches|function '_draw_graph' has 9 branches (max 8) +game/presentation/views/level_map_screen.gd|quality/max-class-variables|class has 21 variables (max 15) +game/presentation/views/level_map_screen.gd|quality/max-function-length|function '_draw_graph' is 78 lines long (max 50) +game/presentation/views/level_map_screen.gd|quality/max-function-length|function '_init' is 135 lines long (max 50) +game/presentation/views/level_map_screen.gd|quality/max-local-variables|function '_draw_graph' has 27 local variables (max 10) +game/presentation/views/level_map_screen.gd|quality/max-local-variables|function '_init' has 21 local variables (max 10) +game/presentation/views/settings_overlay.gd|quality/max-function-length|function '_build' is 77 lines long (max 50) +game/presentation/views/settings_overlay.gd|quality/max-local-variables|function '_build' has 13 local variables (max 10) +game/presentation/views/standee_body.gd|quality/allocation-in-loop|object allocation '.new()' inside a loop +game/presentation/views/standee_body.gd|quality/max-class-variables|class has 29 variables (max 15) +game/presentation/views/standee_body.gd|quality/max-function-length|function '_draw' is 84 lines long (max 50) +game/presentation/views/standee_body.gd|quality/max-local-variables|function '_draw' has 26 local variables (max 10) +game/presentation/views/ui_kit.gd|quality/max-local-variables|function 'beams_rect' has 13 local variables (max 10) +game/presentation/views/ui_kit.gd|quality/max-parameters|function '_init' has 6 parameters (max 5) +game/presentation/views/ui_kit.gd|quality/max-parameters|function 'bcard' has 6 parameters (max 5) +game/presentation/views/ui_kit.gd|quality/max-parameters|function 'modal_shell' has 6 parameters (max 5) +game/presentation/views/ui_kit.gd|quality/max-public-methods|class has 26 public methods (max 20) diff --git a/tools/gdstyle_gate.py b/tools/gdstyle_gate.py new file mode 100644 index 00000000..ad89025b --- /dev/null +++ b/tools/gdstyle_gate.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""gdstyle_gate.py — S13 第三方质量规则闸件(game/tests/static_scan.sh 调用) + +跑 gdstyle(github.com/atelico/gdstyle,Rust 单二进制,版本钉 v0.4.0)质量规则扫描, +与存量基线 tools/gdstyle_baseline.txt 做差集——只有基线外的新违规才红。 +规则口径见 tools/gdstyle.toml 头注(只开 quality/*+syntax/reserved-identifier, +format/naming/order 全关)。扫描面=产品码三目录(game/core、game/data、game/presentation, +与 S9 类型闸同口径——probe/sim/tests 测量面直构惯例不在面)。 + +基线键=文件|规则|消息 三元组:消息含违规对象标识(如 duplicated load 的路径),行漂移稳定 +(gdstyle 的 JSON 输出不含源码行文本,行号键会随编辑成片假红)。基线重生成=人工专用 +(--refresh),禁在闸内自动重生成(防清基线换绿)。 + +二进制:本机固定路径 D:/code/gdstyle/gdstyle.exe(不入库不入包),GDSTYLE_BIN 环境变量覆盖; +版本与钉定版不符即红——升级工具须重测语法兼容性并人工重生成基线。 + +退出码:0=无基线外新违规;1=有新违规/二进制缺失或版本不符/扫描面为空/输出不可解析;2=用法错。 +用法:python tools/gdstyle_gate.py # 闸判定 + python tools/gdstyle_gate.py --refresh # 人工重生成基线(覆盖旧文件) +""" +import json +import os +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +CONFIG = ROOT / "tools" / "gdstyle.toml" +BASELINE = ROOT / "tools" / "gdstyle_baseline.txt" +SCAN_DIRS = ["game/core", "game/data", "game/presentation"] +PINNED_VERSION = "0.4.0" +DEFAULT_BIN = "D:/code/gdstyle/gdstyle.exe" +MAX_REPORT = 50 + + +def fail(msg: str) -> int: + print("[gdstyle_gate] FAIL — %s" % msg) + return 1 + + +def run_gate() -> int: + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="replace") + + refresh = "--refresh" in sys.argv[1:] + if sys.argv[1:] and not refresh: + print("[gdstyle_gate] 用法: python tools/gdstyle_gate.py [--refresh]") + return 2 + + binary = os.environ.get("GDSTYLE_BIN", DEFAULT_BIN) + try: + ver = subprocess.run([binary, "--version"], capture_output=True, text=True, timeout=30) + except (FileNotFoundError, OSError): + return fail("gdstyle 二进制不可用:%s(本机固定路径,GDSTYLE_BIN 可覆盖;" + "v%s Windows 版见 github.com/atelico/gdstyle releases)" % (binary, PINNED_VERSION)) + if PINNED_VERSION not in (ver.stdout or ""): + return fail("gdstyle 版本不符:钉定 v%s,实得 %r——升级工具须重测语法兼容性并人工重生成基线" + % (PINNED_VERSION, (ver.stdout or "").strip())) + + face = [p for d in SCAN_DIRS for p in Path(ROOT, d).rglob("*.gd")] + if not face: + return fail("扫描面为空——%s 下零个 .gd(路径写错导致的静默空转)" % "、".join(SCAN_DIRS)) + + try: + proc = subprocess.run( + [binary, "check", "--format", "json", "--no-color", "-c", str(CONFIG)] + SCAN_DIRS, + capture_output=True, text=True, timeout=300, cwd=str(ROOT), encoding="utf-8", + ) + except subprocess.TimeoutExpired: + return fail("gdstyle 扫描超时(300s)") + try: + findings = json.loads(proc.stdout or "[]") + except json.JSONDecodeError: + return fail("gdstyle 输出不可解析为 JSON(stderr 头部:%r)" % (proc.stderr or "")[:200]) + + keys = set() + for f in findings: + if not isinstance(f, dict) or "rule" not in f: + continue + file_path = str(f.get("file", "")).replace("\\", "/") + keys.add("%s|%s|%s" % (file_path, f["rule"], f.get("message", ""))) + + if refresh: + lines = [ + "# gdstyle 存量基线(S13)——文件|规则|消息 三元组,每行一条;只记基线不记豁免理由。", + "# 重生成=人工专用:python tools/gdstyle_gate.py --refresh(禁清基线换绿;改规则口径/工具升级须随批重生成)。", + ] + lines += sorted(keys) + BASELINE.write_text("\n".join(lines) + "\n", encoding="utf-8") + print("[gdstyle_gate] 基线已重生成:%s 条(%s)" % (len(keys), BASELINE.name)) + return 0 + + base = set() + if BASELINE.exists(): + for line in BASELINE.read_text(encoding="utf-8").splitlines(): + s = line.strip() + if s and not s.startswith("#"): + base.add(s) + else: + return fail("基线文件不存在:%s(首次落位跑 python tools/gdstyle_gate.py --refresh,须经用户批准)" % BASELINE.name) + + new = sorted(keys - base) + print("[gdstyle_gate] 扫描面 %d 个 .gd;命中 %d 条,基线内 %d 条,基线外新违规 %d 条" + % (len(face), len(keys), len(keys & base), len(new))) + if new: + for k in new[:MAX_REPORT]: + print(" NEW " + k) + if len(new) > MAX_REPORT: + print(" ...(截断,共 %d 条)" % len(new)) + return fail("基线外新违规 %d 条——新码须修到基线口径内,或经用户批准随批重生成基线" % len(new)) + print("[gdstyle_gate] ok") + return 0 + + +if __name__ == "__main__": + sys.exit(run_gate()) diff --git a/体验验证清单.md b/体验验证清单.md index 435dc00d..6b6f6402 100644 --- a/体验验证清单.md +++ b/体验验证清单.md @@ -348,3 +348,33 @@ | 5 | 空槽新档链 | 空槽点「新档」→进入营地,槽卡就地翻为有档形态(进档+删档钮) | - **红灯信号**:主菜单出现旧六钮任何一枚;选档层摘要读出别槽值;删档一击直删;旧档迁移后槽 1 白板(迁移没跑)。 - **结论回流**:观感问题归迭代 34 卡④(营地场景图重立卡顺带复核入口观感);数据层缺陷归待办清单营地交互重做条。 + +--- + +## ㉜ 迭代 34——遭遇行集换代与信息迷雾实机目检(2026-10-24 随反哺写入登记) + +> 卡③ 把遭遇寻址换成(关卡序号, 行类, 深度带)三参,卡④ 给关卡节点图加信息迷雾(邻近 N 层内显示类型)。机检已覆盖行集解析、校验负向判据与可见性/绘制记录断言,遭遇观感与雾下图面仍要人眼看。登记不阻塞(2026-10-12 真人检测立规);总收口时点=迭代 34 实现报告(已退役,git 可考)。 + +| # | 目检项 | 判据 | +|---|---|---| +| 1 | 三关实机开战遭遇观感不回归(卡③) | 第一关地精/熊地精、第二关巨蜘蛛与重击手混编、三关 Boss 战的敌人数量与外观与改动前一致 | +| 2 | 雾下可读性观感(卡④) | 未知类型房间一眼可辨为「未知」而非「漏画」——编号仍在,类型图标与文字不出现 | +| 3 | 多终点 Boss 标记观感(卡④) | 双 Boss 图上两个终点的紫赤描边都清楚可辨 | +| 4 | 大图完整可见观感(卡④) | 大图(11 层×6 槽)整幅落在画布内,无节点被裁切或挤在一角 | +- **红灯信号**:改动前后遭遇敌人数量/外观不一致;雾下房间读作漏画或类型文字漏出;双 Boss 只有一侧带描边;大图有节点出界。 +- **结论回流**:实现缺陷归待办清单登记新条目;观感问题归关卡节点图呈现面后续批。 + +--- + +## ㉝ 迭代 35——生成图换代实机目检(2026-10-25 随收尾写入登记) + +> 两卡把图的来源从模板池换成「参数档×种子」生成图:卡① 落生成器与产出规格,卡② 切消费侧并清出 15 张模板。机检已覆盖生成图确定性、深度带覆盖、商店三性质、断点代差拒绝等;选路观感、首局教学图与画幅观感仍要人眼看。登记不阻塞(2026-10-12 真人检测立规);总收口时点=迭代 35 实现报告(已退役,git 可考)。 + +| # | 目检项 | 判据 | +|---|---|---| +| 1 | 三关选路观感不回归(卡②) | 第二/三关开局即出现两个可选入口且都能进入;岔口候选互异且分属不同支路;走 A 支只遇 A 支终点 Boss;每个终点前恰一家店 | +| 2 | 首局教学图体验(卡②,种子 42 固定图) | 新档开局(`is_first_adventure_done==false`)每次都是同一张图;入口战→岔口→商店→Boss 的顺序可读,含至少一个奖励位与一个恢复位 | +| 3 | 三关生成图选路观感(卡①) | 图规模与分支形态逐关变大;岔口有实际取舍,不是「只有一条路能走」 | +| 4 | 大图完整可见观感(卡①+画布缺口) | L2/L3 生成图整幅落在 1280 屏内、节点不被裁切且都可点到——已知冻结画布公式在 L3 深度域 0–13 下可能超宽,本项同时验证该缺口是否已在实机显形 | +- **红灯信号**:第二/三关只有一个入口或第二入口不可进入;岔口候选同类型或同支路;走 A 支遇到 B 支 Boss;教学图每次开局不同;节点出屏或点不到;三关图规模不递增。 +- **结论回流**:实现缺陷归待办清单登记新条目;观感与画幅问题归呈现批(待办清单「超预算图策略升级路径」条)。 diff --git a/待办清单.md b/待办清单.md index a55c953a..db63e086 100644 --- a/待办清单.md +++ b/待办清单.md @@ -2,6 +2,95 @@ 本清单是流水线的唯一登记处:要做的事先在此登记一条,随流水线段推进改状态,做完整条删除;各系统文档的「待办」节只留一行指路存根,不再自建条目。条目格式(标题=登记元数据,正文=现状/缺口/归属/状态四属性)与状态机以 [doc-conventions §12.1](design/engineering/doc-conventions.md) 为权威。 +#### 超预算图策略升级路径——等比缩放与滚动视口两案待裁(2026-10-24 登记,来源=批次移交,设计面=文档轨) + +**现状**:迭代 34 卡④(信息迷雾可见性批,已合回 master,提交 16a7ec80)按「参数限图」交付,图包络约 10 层×8 槽即当前冻结画布的屏预算上限。若后续定值批需要更大的完整可见图,两条升级路径都要动冻结件,均未设计:①等比缩放——动「节点图标 20px 烘焙 1:1」与「通道折线冻结 2/3/4px」两条冻结值;②滚动视口——新增视口交互范式。迭代 35 生成图实测深度域 L3 达 0–13,已超出该上限(level-map 已登记随动缺口,本批不裁方案)。 +- 相关文档 design/gdd/systems/冒险循环/关卡节点图生成.md v1.3(调参表「图包络」行) +- 相关文档 design/gdd/ux/level-map.md(画布冻结公式) + +**缺口**:两案需用户裁决后再走设计讨论;未裁前禁改画布冻结件。 + +**归属**:[关卡节点图生成](design/gdd/systems/冒险循环/关卡节点图生成.md)(图包络约束)+ [level-map](design/gdd/ux/level-map.md)(画布与交互呈现) + +**状态**:待文档设计 + +#### game 代码结构审查批——行为缺陷定点修+结构重构候选(2026-10-24 登记,来源=审计发现,设计面=工程直入) + +**现状**:审计报告=reports/审计-game代码结构审查-2026-10-24.md(2026-10-24,全目录只读审查,零测试执行、结论全部静态阅读)。缺陷证据(报告 §二 Critical 行为缺陷段+定点清单,含行号)九件:①core/damage_controller.gd:215 用 `==` 比较单位字典(Godot 4 语义=逐键内容比较)——内容全同双敌其一死亡误作废对方单据,应 `is_same()`;②presentation/views/standee_body.gd:172/188 蓄力意图档常量错配(GOLD_INK vs GOLD_DEEP)死代码,三档运动签名设计失效且无测试覆盖;③presentation/views/battle_end_overlay.gd:281-287 庆祝 tween set_loops 无限叠加泄漏(close 只隐藏不 kill);④批量模拟×两层调试并发互斥缺口(presentation/views/debug_overlay.gd:84+presentation/main.gd:712-715/1489+presentation/tune_sim_controller.gd:46-58——用户两击可并触发,演出扭曲+测量作废);⑤presentation/modal_stack.gd:23-29 未登记层/重复开层静默降级(违 fail-fast,当前调用面无违规者属埋雷);⑥core/minesweeper_rules.gd:8-9 两条核心信号零契约成文(违 code-conventions §2.1)+全库信号注释载体普遍 `#` 非 `##`;⑦明细行类型封闭枚举漂移(core 实发 13 类 vs battle_orchestrator.gd 头注 9 类);⑧core/level_runner.gd:895/779 玩法数值字面量(relic 基价 15.0、散折 0.8,自注 provisional);⑨presentation/views/battle_stage_view.gd:332-338 以 display_name 子串匹配解析受击目标(同名敌错位,应携带 enemy_id)。另有定点小件与结构重构候选(报告 §三):main.gd 1508 行可判离块 ~440 行/board_view.gd 1219 行瓦片算法+英雄精灵可拆/adventure_runner 令牌池子系统 ~500 行可拆 CurioDeck/level_runner 商店子系统 ~260 行可拆/断点 schema 三处重复登记可单点化——重构候选非缺陷,是否立项由写卡段呈报用户裁决。**2026-10-24 写卡会话收口**:用户裁五件重构候选全部立项(卡①缺陷修先行落地后重构再动);缺陷⑦明细行枚举漂移划归「架构与规范对齐批」条目(词表 13/15 归该批裁决,缺陷卡不碰头注);报告 §三「其余小件」(level_map 视图直读/BFS、main 死变量与 confirm 旁路、settle_batch 空判、row_pacer 常驻 flush、_fatal assert 剥离、rng_service early-return)未立项,留审计报告在册待后续点名。切卡输入=审计报告 §二/§三,四卡均零待裁点。 + +关联文档清单: +- 需求卡片 reports/需求卡片-game行为缺陷定点修批-2026-10-24.md(2026-10-24 v1.2(grilling 两轮复核通过),缺陷修主卡·迭代批,八件缺陷+定点小件七件,零待裁点) +- 需求卡片 reports/需求卡片-重构main判离批-2026-10-24.md(2026-10-24 v1.2(grilling 两轮复核通过),重构卡 R1·迭代批,零待裁点) +- 需求卡片 reports/需求卡片-重构boardview拆件批-2026-10-24.md(2026-10-24 v1.2(grilling 两轮复核通过),重构卡 R2·迭代批,零待裁点) +- 需求卡片 reports/需求卡片-重构runner拆件与断点schema单点批-2026-10-24.md(2026-10-24 v1.2(grilling 两轮复核通过),重构卡 R3·迭代批,零待裁点) + +**归属**:game/ 呈现层与 core 域(缺陷定点修,迭代批);结构重构候选另立批次由用户裁决。切卡输入=审计报告 §二/§三 + +**状态**:待写卡+已写卡 + +#### S9 类型闸正则失灵修复——R2 恒零命中+static 函数漏网(2026-10-24 登记,来源=审计发现,设计面=工程直入) + +**现状**:工程债证据=reports/审计-game代码结构审查-2026-10-24.md §二 C1(已独立复现):tools/audit_type_hints.py:29 的 FUNC_DECL 正则两洞——①无返回类型声明按 GDScript 语法必然以 `:` 结尾,正则要求 `)` 后只跟 `->`/注释/行尾,R2 目标形态 100% 失配(R2 从上线起恒零命中恒绿);②`^(\s*)func` 不认 `static ` 前缀,static 函数对 R2/R3 整体漏网。实证存量(闸当前退出码 0):core/level_runner.gd:632、core/attunement.gd:10、presentation/main.gd:565、presentation/views/hud_presenter.gd:10-11、core/reward_economy.gd:104/119/131/146、core/unit_container.gd:275(写卡会话路径订正:hud_presenter 实址 game/presentation/hud_presenter.gd,非 views/)。属架构文档测试域自警的「规则扫描面为空=恒绿」静默失效模式(零命中自检因 R1/R3 仍有命中能力未拦住)。 + +关联文档清单: +- 需求卡片 reports/需求卡片-S9类型闸正则修复批-2026-10-24.md(2026-10-24 v1.2(grilling 两轮复核通过),非迭代批,零待裁点;存量清单以开工时盘面清点,实证九址为已知下限) + +**归属**:tools/ 静态扫描域(闸件批,非迭代批——gdstyle 引入批 2026-10-24 已按同款形态收口:先摸存量、闸改动配撤销证明) + +**状态**:待写卡+已写卡 + +#### S13 gdstyle 免名单把数字当身份——存量债改小也报「基线外新违规」(2026-10-25 登记,来源=批次移交,设计面=工程直入) + +**现状**:工程债证据=迭代 35 卡② 合回前后实测(master 合回笔 `5499eb9c`)。闸件 `tools/gdstyle_gate.py:82` 把每条违规登记成三元组「文件|规则|消息全文」,而 gdstyle 的消息文本里带具体数字(如 `function 'resume_adventure' is 104 lines long (max 50)`),基线 `tools/gdstyle_baseline.txt`(241 行)按整句逐字比对——**只要某函数的行数/分支数/变量数发生变化,老违规就匹配不上,被判「基线外新违规」并判红,改小也算新增**。卡② 一批即因此冒出 12 项假新增,逐条对基线旧值核实全是存量债的计数漂移:`config_db_cross_validator.cross_validate` 159→107 行/31→21 分支/39→24 局部/嵌套 6→5(本批按卡片删除跨表校验④整段所致,债变小)、`level_runner` 类变量 40→38 且其「文件 1013 行超长」违规已消失(债被还掉)、`start_level_external` 参数 8→7、`main.gd` 1509→1507、`adventure_runner` 类变量 43→42/文件 1050→1051/`resume_adventure` 104→108 行(本批加断点代差标记键拒绝检查多 4 行)。本批真正新写的超标仅 1 处(新函数 `validate_graph` 72 行/20 分支),已当场拆成四段修掉。**盘面后果**:卡② 按用户令零复跑合回,master 尖 5499eb9c 从未整套跑绿过代码域闸;下一次任何批跑代码域闸都会看到 S13 红这 12 项(不处理则每批重复一次「要么重生成基线、要么被红挡在合回前」)。 + +**预计改动面(两档,第一条只治标)**:① 治标=人工跑一次 `python tools/gdstyle_gate.py --refresh` 重生成基线+一笔提交,约 5 分钟,但键形态未改,下一批再动这些文件还会重演;② 治本=改 `tools/gdstyle_gate.py` 的键构造段(:77-90,约 10–20 行)为「文件|规则|函数名」——函数名从消息里正则提取 `function 'x'`,无函数名的条目(类变量数/文件长度)退化为「文件|规则」,并**另加一条恶化判据**防新键吞掉真问题(同一函数当前指标较基线登记值恶化超阈值即判红,纯规范化数字会把 104→300 行的恶化也放过去,不可只靠数字抹平);随动=重生成 `tools/gdstyle_baseline.txt`(241 行按新键重排)、`tools/run_all_gates.sh` 头注与 `game/tests/README.md` 工具速查表的 S13 描述各一行、撤销证明四项(新增一处真超标必红/同函数改小改大的数字漂移禁止再冒新/恶化超阈值必红/禁闸内自动重生成清基线换绿的自检仍绿——各配撤销该判据见红+还原复绿)。合计约 **0.5 个工作日**,非迭代批(tools/ 闸机与文档面),零待裁点即可写卡。 + +**归属**:tools/ 静态扫描域(闸件批,非迭代批)——与「S9 类型闸正则失灵修复」「S6/S7/S8 静态规则落地」两条同族:规则扫描面自身失灵/失准,防线不可信即等于没有防线 + +**状态**:待写卡 + +#### S6/S7/S8 静态规则落地——编排唯一性/禁 await/信号纪律三闸(2026-10-24 登记,来源=审计发现,设计面=工程直入) + +**现状**:工程债证据=reports/审计-game代码结构审查-2026-10-24.md §二 C2:三规则 static_scan.sh TODO 在案(架构文档静态扫描表自认未落地)——S6 core↔core 信号禁 lambda 捕获 self、S7 B2/B3/B4 API 符号命中文件 ⊆ B1 文件集、S8 结算路径 await 零命中。2026-10-24 审查人工 grep 结论干净(core 零 await、A2/B3/B4 零相互直调),但防线缺位=回归无闸。S7 前置:须先在架构文档成文 B1 文件集边界(含 core/settlement/ 五段类与 damage_controller——现归属表零条目,见「架构与规范对齐批」条目①)。**2026-10-24 用户裁:条目整条冻结待④(「架构与规范对齐批」)裁决后再写卡**——写卡会话核对结论:S6(core 信号禁 lambda)与 S8(结算路径禁 await,文件集可按现行结算文件直接枚举)与④零依赖、技术上可先行,但按「先裁④再动③」整条冻结不先行。 + +**归属**:tools/ 静态扫描域(非迭代批);S7 依赖「架构与规范对齐批」先裁 B1 文件集 + +**状态**:待写卡 + +#### 验证集清单快照重生成——断言索引含退役措辞与旧行号(2026-10-25 登记,来源=审计发现,设计面=工程直入) + +**现状**:工程债证据=迭代 35 卡② 验收审计申报(git 历史可考:合回提交 5499eb9c 内的 reports/iter35-卡②-验收审计-2026-10-24.md,该产出物已按吸收即死退役):game/tests/验证集清单.md 是批 B 迁移时的断言文本快照索引,只由 tools/extract_checks.py 手工重生成、不进任何闸;卡② 断言面换代后十余行仍含 first_script/template_pick 等退役措辞与过期行号(实测 first_script 8 处、template_pick 2 处),与宿主现文不符。 + +**归属**:game/tests/验证集清单.md+tools/extract_checks.py(重生成或清单头注记两路,写卡时定) + +**状态**:待写卡 + +#### 测试宿主改名——node_map_templates_test.gd 已承载生成图确定性断言(2026-10-25 登记,来源=审计发现,设计面=工程直入) + +**现状**:工程债证据=迭代 35 卡② 反哺点⑧:game/tests/integration/node_map_templates_test.gd 现承载「生成图确定性断言套」,文件名中的 templates 是模板时代遗留;manifest 登记行=game/tests/run_all.gd:90。改名为 node_map_determinism_test.gd 须同批改 manifest 路径行。 + +**归属**:game/tests/integration/+game/tests/run_all.gd manifest + +**状态**:待写卡 + +#### level.tres 表尾注释含迭代叙事(2026-10-25 登记,来源=审计发现,设计面=工程直入) + +**现状**:工程债证据=迭代 35 卡② 反哺点⑨:game/data/tables/level.tres 表尾注释写「旧 15 张模板池(3 关 × 5)随迭代 35 卡② 清出」,与 doc-conventions §5 禁写变更叙事有张力;卡② 卡片 T3 明令表尾写模板去向注记,故处置面限于叙事形态(删迭代号、保留图源换代事实句),不删去向事实。 + +**归属**:game/data/tables/level.tres + +**状态**:待写卡 + +#### 架构与规范对齐批——代码结构审查发现的文档-实现漂移(2026-10-24 登记,来源=审计发现,设计面=文档轨) + +**现状**:审计报告=reports/审计-game代码结构审查-2026-10-24.md(§二 C3/C4/C5+§三 漂移清单)。漂移与待裁件七件:①架构文档「模块归属」多行 API 清单与实现不符(A2 try_* 便捷形/B2 ambush_grant_plan/B4 equivalence_supply 不存在;实有 grant_reserve_ap/finish_round_stop/check_skill_unlocks 未登记;damage_controller 与 settlement/ 五段类零条目——S7 落地的前置);②code-conventions §1.3 核心写调用位置——三拆件控制器(presentation/curio_interact_controller.gd、camp_shop_presenter.gd、treasure_interact_controller.gd)持写调用 vs 规范「只允许在组合根出现」的 spec-AC 冲突,须裁「改规范成文组合根家族」vs「调用收回 main」;③A3→EffectEngine 反向依赖(core/action_economy.gd:43-49)——真违约或事实源失真二选一(effect_engine 降层登记基础层 vs B1 代发 vs 仅补登记);④架构文档 :149/151 层合同计数写死 17 项 vs LAYER_CONTRACT 实 18 项;⑤明细行协议三处三个数(头注 9/实发 13/架构文档 15);⑥§1.3 例举 submit_window_selection 符号已退役;⑦core/effect_engine.gd:360 校验文案「9 项」实为 18 项。 + +**缺口**:一次 doc-design 会话逐点裁决(②③有方案取舍须用户拍板,①④⑤⑥⑦为机械对齐);裁决后走非迭代批落文 design/engineering/。 + +**归属**:design/engineering/(architecture.md「模块归属」节+「呈现层装配」节、code-conventions §1.3) + +**状态**:待文档设计 + #### 测试术语与层级重构批 A——测试命名+频率规则+新冒烟测试(2026-10-22 登记,来源=会话提及,设计面=工程直入) **现状**:测试域名实分离(名叫冒烟的载体实为千项断言面)、每轮跑闸频率过高、测试金字塔倒挂的规则重构,合同=reports/提案-测试术语与层级重构方案-2026-10-22.md(v2,2026-10-22 用户裁决全部闭合、待裁点清零)。批 A=非迭代批(规则与闸件批),用户 2026-10-22 直接下开工令执行:待办清单登记 → 改规则(process-conventions/roadmap loop 规范/术语表退役词对照/AGENTS.md 规则行)→ 写新冒烟测试(game/tests/smoke_boot.tscn/.gd+tools/run_smoke.py 重写)与系统测试宿主(tools/run_system_test.py+game/tests/system_test.gd 薄壳)→ 改闸机键映射(新增 00 冒烟测试、01 改系统测试、02 改单元与集成测试)→ 改描述性文档与 iter 技能三副本 → 全量域闸验证。批 B(断言迁移,用户特批免卡直改 game/)停机等用户发令,不在本条执行面。 @@ -110,32 +199,6 @@ **状态**:待文档设计(备选) -#### 关卡节点图重构——可选节点太少(2026-10-13 登记,来源=playnote,设计面=文档轨) - -**现状**:来源=playnote 用户试玩记录「关卡节点图展示得有问题,希望可以优化一下。可以参考哥布林弹珠的那种方式去移动」。2026-10-13 用户明确定性:**这是当前关卡节点图的重构,不是呈现优化**——理由是**现在的可选节点太少**(用户原话),所以不是换一种画法或移动方式就能解决的事。 - -关联文档清单: -- 设计定稿 design/gdd/systems/冒险循环/关卡节点图生成.md v1.1(2026-10-21) -- 设计定稿 design/gdd/systems/冒险循环/关卡节点地图.md v2.1(2026-10-21) -- 需求卡片 reports/需求卡片-节点图生成器核心批-2026-10-21.md(2026-10-21) -- 需求卡片 reports/需求卡片-节点图消费侧换代批-2026-10-21.md(2026-10-21) -- 需求卡片 reports/需求卡片-敌人表深度带换代批-2026-10-21.md(2026-10-21) -- 需求卡片 reports/需求卡片-信息迷雾可见性批-2026-10-21.md(2026-10-21) - -**现行实态**(`level.tres` 实查,三关各 5 张模板):**第一关五张模板全是单链排列变体**——7 个节点=战斗 ×4 + 奖励 + 休整 + Boss,战斗深度序单调保难度坡,**变化面只有奖励与休整两位的先后次序**,全链零分叉,玩家在选路上**没有任何选择**;第二/三关是网状图(10 节点,带 `next` 分支,最大层宽 4–5),有分叉,但用户仍判选择量不足。参考方向=Peglin(哥布林弹珠)的节点图移动与选路方式。 - -**范围(重构面,非呈现面)**:节点数量、分叉度、选路自由度。连带三面——① 关卡节点图生成(旧版文档已删、随重构重写;现行 15 张骨架=3 关 ×5 已判过期,随重构清理);② [level-map](design/gdd/ux/level-map.md)(选路枢纽三停点布局定稿版,图结构改则屏面随之改);③ [敌人系统](design/gdd/systems/战斗循环/敌人系统.md)(遭遇表按 `enemy.tres` 的 (level_index, node_depth) 坐标索引,节点数与编号一改,行集坐标与首遇教学位断言面全部随动)。 - -**量化要求补登(2026-10-16 playnote 用户补写)**:「需要统计一下第 123 关 path 的路径长度,路径长度需要递增,并且后续关卡的节点数增幅应该更大」(第 123 关=第 1/2/3 关)——已定稿成验收形态:读数=最长路径步数与节点总数(口径见[关卡节点图生成](design/gdd/systems/冒险循环/关卡节点图生成.md)验收标准),两者逐关递增、节点总数增幅 Δ(L2→L3)>Δ(L1→L2);基线采集发起权归用户,读对象=生成图(旧模板已裁不作参考)。 - -**前置(用户 2026-10-13 裁)**:**须先做文档设计讨论,不可直接开发**——参考调研+重构方案先行、过人闸后才可开卡。版本标准侧已登记为 Alpha 待做项,见 [08-Alpha版本标准](design/gdd/08-Alpha版本标准.md)「一 · 系统 · 冒险循环」。 - -**缺口(2026-10-24 迭代 35 开工检查登记)**:迭代 35 停机退回——卡①② 已于同日 v1.1 修订重锚断言宿主(game/smoke/ 退役后现态,卡① 可独立成批开工);卡② 阻塞于「生成图深度扩面覆盖批」未立卡——生成图深度域(L1 0–6/L2 0–9/L3 0–13,卡① §8.1 provisional)未被敌带与板键覆盖(敌侧战斗带顶 L1=3/L2=L3=7、Boss 各关单带 4/9/9;板侧 board_gen `node_depth` 精确行同域缺口),且 L2/L3 多终点 Boss 各终点深度需解析带键可互异;切源即 `band_of` 越顶 -1 加载致命。立卡要件(归口=卡③ §5):敌侧每 (关, row_kind) 顶带键 ≥ 该关最大生成深度+Boss 带键可互异;板侧 board_gen 键域覆盖生成深度全域;数值 provisional。覆盖批落地前卡② 不得切源合入(卡② §0 硬交付序列),落地后重编迭代交付卡①②。 - -**归属**:[关卡节点地图](design/gdd/systems/冒险循环/关卡节点地图.md)(图结构与节点语义,主责)+ [关卡节点图生成](design/gdd/systems/冒险循环/关卡节点图生成.md)(产出规格与生成参数)+ [level-map](design/gdd/ux/level-map.md)(选路呈现)+ [敌人系统](design/gdd/systems/战斗循环/敌人系统.md)(遭遇行集坐标连带);版本标准侧已登记为 Alpha 待做项,见 [08-Alpha版本标准](design/gdd/08-Alpha版本标准.md)「一 · 系统 · 冒险循环」 - -**状态**:文档设计已闭合+已写卡+已编迭代 34+已编迭代 35 - #### 扩展节点类型——危险/精英节点与特殊地形节点(2026-10-21 登记,来源=会话提及,设计面=文档轨) **现状**:用户 2026-10-21 会话定方向:Alpha 版只留现行四类(战斗/奖励/恢复/商店),以下两类留到以后再做——①危险/精英节点(怪物与普通战斗不同、奖励肥);②特殊地形节点(效果修饰:地图战斗资源、敌我伤害、金币比例等)。内容未设计。