diff --git a/design/gdd/00-游戏概念.md b/design/gdd/00-游戏概念.md index c246e504..4c9c79ad 100644 --- a/design/gdd/00-游戏概念.md +++ b/design/gdd/00-游戏概念.md @@ -34,7 +34,7 @@ | 标错 | **控制失败的失控反噬**——机制上的随机读作"失控",反噬流向不可控 | | 道具 / 奖励 | 前人探险者的遗落物(氛围背景句,登记不锁;宝箱呈现只绑定可使用道具类奖励) | -**存疑项**(未拍板,此处只登记不展开):惩罚格未来类型(陷阱/岩浆/冰冻等)只登记了名字,机制分化未定;标错反噬的具体机制落点(流向谁、一次还是每轮)未定,本文只承诺叙事壳;两类增伤分量的叙事称呼("地利/暴露")仍是候选措辞,未拍板。 +**存疑项**(历史登记,此处只留痕不展开):惩罚格未来类型(陷阱/岩浆/冰冻等)——2026-09-20 首批四类型已定稿落地(荆棘丛/藤蔓缠绕/岩浆/烬覆,见[地形变异](systems/排雷循环/地形变异.md)),其余未来类型仍只登记名字;标错反噬的具体机制落点(流向谁、一次还是每轮)未定,本文只承诺叙事壳;两类增伤分量的叙事称呼("地利/暴露")仍是候选措辞,未拍板。 ## 术语层级 diff --git a/design/gdd/02-系统清单.md b/design/gdd/02-系统清单.md index ba391148..f1f88e48 100644 --- a/design/gdd/02-系统清单.md +++ b/design/gdd/02-系统清单.md @@ -24,7 +24,7 @@ | `棋盘生成.md` | 棋盘怎么生成、怎么保证可推理可通关 | 已定稿·自旧体系迁移 | | `排雷玩法.md` | 挖格、插旗、连锁展开等扫雷规则本体 | 已定稿·自旧体系迁移 | | `行动经济.md` | 每轮行动点怎么花、停手与刷新的代价 | 已定稿·自旧体系迁移 | -| `地形变异.md` | 格子的非雾变异(岩浆/冰冻等,待定) | 已定稿·自旧体系迁移 | +| `地形变异.md` | 格子的非雾变异(首批四类型:荆棘丛/藤蔓缠绕/岩浆/烬覆) | 已定稿·首批类型落地 | | `魔法道具.md` | 扫雷侧魔法道具(显影、探测类) | 已定稿·自旧体系迁移 | ### 战斗循环(`systems/战斗循环/`) diff --git a/design/gdd/03-内容规模.md b/design/gdd/03-内容规模.md index 364cb79c..0bb2bd26 100644 --- a/design/gdd/03-内容规模.md +++ b/design/gdd/03-内容规模.md @@ -25,7 +25,7 @@ MVP 不加新敌种,也不做同深度多敌种混搭;敌种扩容走数据 **两档尺寸**(档位概念是设计决策:小板 / 大板,Boss 战的台阶靠换大板制造),具体尺寸面值与雷密度读 `data/tables/board_gen.tres`(registry 为准)。密度全程一档,不做密度爬坡;可推理率目标全程恒定,目标值归[排雷玩法](systems/排雷循环/排雷玩法.md)口径。 -地形变异暂无内容:岩浆/冰冻等只登记了名字,机制未定(见 `data/tables/variation.tres` 的登记现状)。 +地形变异首批四类型已落地(2026-09-20 首批类型定稿批):荆棘丛/藤蔓缠绕(第一关)+ 岩浆/烬覆(第二关),行值见 `data/tables/variation.tres`。 ## 战斗道具(格面资源) @@ -51,7 +51,7 @@ MVP 不加新状态;扩容走 status_effects.tres 加行 + registry 登记。 - 更多关卡(N>2)——第二层落地后,第三关起走扩展批流程 - 精英节点与词缀系统——只预留了字段位,不定义语义 -- 地形变异内容——岩浆/冰冻等只登记了名字,机制未定 +- 地形变异扩展内容——首批四类型已落地,反制道具与全局作用域排 Alpha(2026-09-20 用户拍板) - 道具/增益之间的组合协同(遗物式联动) - 战后自动回复——回复只有道具与恢复点两个来源 - 元进度与跨局成长 diff --git a/design/gdd/05-不做清单.md b/design/gdd/05-不做清单.md index 0e7b62ae..7cea494b 100644 --- a/design/gdd/05-不做清单.md +++ b/design/gdd/05-不做清单.md @@ -4,7 +4,7 @@ ## 确定不做(与定位冲突,永久排除) -- **雾格 / 迷雾类战争迷雾机制**:曾在旧设计中承担地形变异,2026-09-14 已从设计中移除。它给推理信息源的代价大于收益,地形变异的未来只走非雾变异(岩浆/冰冻等,待定)。 +- **雾格 / 迷雾类战争迷雾机制**:曾在旧设计中承担地形变异,2026-09-14 已从设计中移除。它给推理信息源的代价大于收益,地形变异只走非雾变异(首批四类型 2026-09-20 落地)。 - **多人 / 在线要素**:游戏前提就是单机单人。合作、对战、排行榜这类在线功能会把开发成本翻倍,还不服务核心受众。 - **重叙事**:不写前人日志、剧情演出这类长篇文本。世界观只做碎片点缀,卖点在玩法不在文本。 - **即时操作玩法**:所有决策都在回合内慢慢想,不考反应、不考手速。任何"限时点击""走位闪避"都免谈。 diff --git a/design/gdd/06-待办清单.md b/design/gdd/06-待办清单.md index 908bb662..5a17bd26 100644 --- a/design/gdd/06-待办清单.md +++ b/design/gdd/06-待办清单.md @@ -19,37 +19,14 @@ ### 地形变异 -#### 首批变异类型(岩浆/冰冻)机制未定 - -**现状**:类型表两行均为「仅登记名字,机制未定(provisional;出处为游戏概念文档的存疑项登记)」(冰冻行出处同上);名字已登记、机制分化未定,定稿批随批配至少一件反制道具。来源:正文 provisional 标注 + P1 后置表 - -**归属**:地形变异首批类型定稿批 - -**状态**:待办(2026-09-19 用户拍板:地形变异相关事项整体改排 **Alpha 版本之后**,垂直切片期不讨论、不做相关实现) - -#### 生成期布置的约束与参数未定稿 - -**现状**:布置约束(变异允许落在什么格子上)与布置参数(摆几个、多大、摆哪)未定稿,将与首批变异类型的机制一起拍板,归属棋盘生成的管线预留段;定稿前,棋盘生成不为变异做任何额外工作。来源:正文 provisional 标注(生成期布置接口节) - -**归属**:棋盘生成(管线预留段),与首批变异类型机制同批拍板 - -**状态**:待办(2026-09-19 用户拍板:随地形变异整体改排 **Alpha 版本之后**,垂直切片期不讨论、不做相关实现) - #### 全局作用域变异 -**现状**:全局作用域变异(精英/Boss 图整图生效的变异形态)——作用域语义未定稿。来源:P1 后置表 +**现状**:全局作用域变异(精英/Boss 图整图生效的变异形态)——作用域语义未定稿。来源:P1 后置表(首批类型定稿批 2026-09-20 落地:首批四类型均为 region 格子级;scope=global 词表位保留) **归属**:[难度曲线与Boss](systems/冒险循环/难度曲线与Boss.md) -**状态**:待办(2026-09-19 用户拍板:随地形变异整体改排 **Alpha 版本之后**,垂直切片期不讨论、不做相关实现) +**状态**:待办(2026-09-19 用户拍板:Boss 图变异暂无设计想法,留 **Alpha 阶段**再设计,当期不处理) -#### 变异格对初始展开的影响语义 - -**现状**:2026-09-18 用户拍板两件挂账之一已清偿——board_data.gd「带变异的 0 格不参与初始展开传播」死分支已删(与排雷玩法展开实现口径归一,冒烟 300 项全绿;变异定稿批按正式语义重接)。剩余面:变异格该不该影响开局免费展开、影响规则是什么,未定稿,随首批变异定稿批一并裁决(变异体系暂缓中) - -**归属**:地形变异首批类型定稿批 - -**状态**:待办(死分支已清偿,语义裁决随变异定稿批;2026-09-19 用户拍板:随地形变异整体改排 **Alpha 版本之后**,垂直切片期不讨论、不做相关实现) #### 显影尘效果重定 **现状**:原效果"显影"为恢复雾下推理原料(雾格数字显影,时效本图持续);雾格已于 2026-09-14 从设计中移除(见[不做清单](05-不做清单.md)),效果整体失义。名称按登记保留,不另起名 @@ -68,11 +45,11 @@ #### 反制维度承载失义 -**现状**:反制维度的唯一承载是显影尘,经由地形变异"雾"行的反制道具绑定(绑定值 [probe_dust]);雾行移除后绑定悬空。"每变异至少一件反制道具"的框架契约本身不受影响 +**现状**:反制维度的唯一承载是显影尘,经由地形变异"雾"行的反制道具绑定(绑定值 [probe_dust]);雾行移除后绑定悬空。"每变异至少一件反制道具"的框架契约本身不受影响。首批四类型(荆棘丛/藤蔓缠绕/岩浆/烬覆)的 counter_items 字段位已预留、恒空回填待本批 -**归属**:地形变异新类型建行批(排 Alpha 版本之后);原与显影尘重定批合流且同排垂直切片收尾后,改排后两批错窗(显影尘重定批排期未动) +**归属**:Alpha 反制批(2026-09-20 用户拍板:反制道具随反制/秘宝类设计整体排 Alpha——垂直切片先把基础循环流程走完;切片期变异暂无反制手段的纯税状态已知悉接受;与显影尘重定批〔排垂直切片收尾后〕的合流关系随两批同窗化自然解除) -**状态**:待办(2026-09-19 用户拍板:随地形变异整体改排 **Alpha 版本之后**,垂直切片期不讨论、不做相关实现) +**状态**:待办(2026-09-20 用户拍板改排 **Alpha 阶段**) #### 探宝罗盘效果重定——两方案评估(2026-09-19 立项) @@ -401,14 +378,6 @@ ### 数据配置 -#### 变异表旧字段集是否沿用 - -**现状**:归档的字段集(信息规则、布置约束、反制道具绑定等)取值语义绑定具体变异类型;现行空清单下字段集沿用与否未定 - -**归属**:[地形变异](systems/排雷循环/地形变异.md)首批类型定稿批 - -**状态**:待办(2026-09-19 用户拍板:随地形变异整体改排 **Alpha 版本之后**,垂直切片期不讨论、不做相关实现) - #### 冒险表未建表(原「关卡表与冒险表未入表清单」关卡表半边已收口) **现状**:关卡表半边已收口(2026-09-18 清理批核销:level.tres + LevelTable schema + config_db 加载行齐备,切片加载清单在册);冒险表 `AdventureTable` 仍未建——registry 已登记字段(keystone_per_level,单行全局,Rule 4 豁免)且注记「未建表零代码影响」,切片无消费方(元进度未实现),建账归冒险结构定稿批 diff --git a/design/gdd/systems/战斗循环/战斗结算.md b/design/gdd/systems/战斗循环/战斗结算.md index a0f70aa0..80670614 100644 --- a/design/gdd/systems/战斗循环/战斗结算.md +++ b/design/gdd/systems/战斗循环/战斗结算.md @@ -65,7 +65,7 @@ | ④ 衰减段 | 移除到期的临时效果;每轮恰好一次,含末轮——清图奖励先兑付、效果后衰减 | | ⑤ 判定段 | 先判主角倒下(负),再判敌人清空(胜),都不满足则继续下一轮 | -结算本体是纯逻辑步骤流:零界面依赖、禁止动画等待,随机消费只走封闭随机流。触发只读行动经济的相位读数与轮末标记,这条消息有棋盘路径和停手路径两个发射源,到达先后不承载语义。这些性质共同保证结算可回放:同样的种子、同样的事件序列,跑出的步骤流逐位复现——录像回放与断言复测都建立在它上面。随机消费恰五处(2026-09-16 敌人技能池批由两处扩为三处;2026-09-17 分支节点图批换板入编排后扩为五处):战斗开始掷骰定敌方实际数量与攻击基线浮动,属于初始化随机、发生在结算步骤流之外;技能摇号属于回合内随机,但时点前置到每轮开轮、同样发生在结算步骤流之外(消费专用 `behavior_roll` 封闭流,每敌按生成序派生独立子流,单敌出招轨迹可独立复放、互不穿插);护甲等级掷点属于回合内随机、发生在步骤流之内(消费专用 `armor_roll` 封闭流,逐次掷逐次取号,可逐位回放);奖励格摇号(消费 `reward_roll` 封闭流,装板期格面资源批派发)与换板棋盘生成(消费 `board_gen` 封闭流,清图轮换板时点)均属初始化随机、发生在结算步骤流之外。五处皆封闭随机流,同种子同输入逐位复现。 +结算本体是纯逻辑步骤流:零界面依赖、禁止动画等待,随机消费只走封闭随机流。触发只读行动经济的相位读数与轮末标记,这条消息有棋盘路径和停手路径两个发射源,到达先后不承载语义。这些性质共同保证结算可回放:同样的种子、同样的事件序列,跑出的步骤流逐位复现——录像回放与断言复测都建立在它上面。随机消费恰六处(2026-09-16 敌人技能池批由两处扩为三处;2026-09-17 分支节点图批换板入编排后扩为五处;2026-09-20 地形变异首批类型定稿批扩为六处——变异布置):战斗开始掷骰定敌方实际数量与攻击基线浮动,属于初始化随机、发生在结算步骤流之外;技能摇号属于回合内随机,但时点前置到每轮开轮、同样发生在结算步骤流之外(消费专用 `behavior_roll` 封闭流,每敌按生成序派生独立子流,单敌出招轨迹可独立复放、互不穿插);护甲等级掷点属于回合内随机、发生在步骤流之内(消费专用 `armor_roll` 封闭流,逐次掷逐次取号,可逐位回放);奖励格摇号(消费 `reward_roll` 封闭流,装板期格面资源批派发)、变异布置(消费 `variant_place` 封闭流,装板与资源预分配之后——棋盘设置顺序第 4 步)与换板棋盘生成(消费 `board_gen` 封闭流,清图轮换板时点)均属初始化随机、发生在结算步骤流之外。六处皆封闭随机流,同种子同输入逐位复现。 举证:五段为流程编排,无计算;段序常量存配置表,加载时校验与定稿顺序完全一致,不一致拒绝启动。 @@ -223,16 +223,16 @@ EV 参考数不入正文(派生读数,需要时按判定式与现行表值 ``` 判定段结果 ├─ 继续(敌未清、主角活着)→ 通知"同图继续" → 下一轮开轮 - ├─ 换图(本轮清图)→ 旧板退役 → 生成新板 → 装板 → 资源预分配恰一次 → 通知"换图" → 下一轮开轮 + ├─ 换图(本轮清图)→ 旧板退役 → 生成新板 → 装板 → 资源预分配恰一次 → 变异布置(棋盘设置第 4 步)→ 通知"换图" → 下一轮开轮 ├─ 战斗结束(胜 / 负)→ 清理敌人与状态 → 发出战斗结束信号 → 休眠,等下一场战斗 └─ 关卡结束(上层显式调用)→ 清空魔法道具背包,给出可见反馈 ``` 停手与耗尽收轮的回合恒为同图继续;换图指令只允许出现在清图轮,其他轮发换图属于程序错误、立即上抛。魔法道具背包以关卡为界:战斗结束不清背包,跨战斗延续;关卡结束时一次性清空,清空前先返回快照计数,作为"剩余魔法道具随关卡结束清空"的可见反馈。战斗道具背包已随可选类删除(2026-09-15)下线。 -挖掘期刷新是玩家在挖掘中途主动换图,经[行动经济](../排雷循环/行动经济.md)扣费:扣费成功后按"旧板退役、生成新板、装板、资源预分配恰一次"编排,本轮不打断——没有结算段、没有轮次结束。储备不足被拒是玩家决策的合法结果,不是编排错误。刷新图不发清图奖励,完美排雷计数随装板重置。 +挖掘期刷新是玩家在挖掘中途主动换图,经[行动经济](../排雷循环/行动经济.md)扣费:扣费成功后按"旧板退役、生成新板、装板、资源预分配恰一次、变异布置"编排,本轮不打断——没有结算段、没有轮次结束。储备不足被拒是玩家决策的合法结果,不是编排错误。刷新图不发清图奖励,完美排雷计数随装板重置。 -战斗开始按固定序列开张:关上配置热重载的闸门、打开增益状态容器、掷骰定敌方实际数量、生成敌人、装板并预分配资源恰一次、发开轮通知并执行开轮摇号(摇定首轮各敌本拍动作)。上一关的大奖励(如血上限提升)经战斗开始入参的覆盖值通道生效,内容归[冒险结构](../冒险循环/冒险结构.md)。 +战斗开始按固定序列开张:关上配置热重载的闸门、打开增益状态容器、掷骰定敌方实际数量、生成敌人、装板并预分配资源恰一次、变异布置([地形变异](../排雷循环/地形变异.md)——棋盘设置顺序第 4 步:生成棋盘 → 打开状态初始化 → 战斗资源格设置 → 变异格设置,消费 `variant_place` 封闭流)、发开轮通知并执行开轮摇号(摇定首轮各敌本拍动作)。上一关的大奖励(如血上限提升)经战斗开始入参的覆盖值通道生效,内容归[冒险结构](../冒险循环/冒险结构.md)。 举证:本节为编排流程,无计算。 @@ -323,7 +323,7 @@ EV 参考数不入正文(派生读数,需要时按判定式与现行表值 - 数值边界:伤害下限保底后全程无零值/负值伤害;溢杀丢弃不转移;治疗封顶不溢出。 - 出口分发:先判主角倒下(负)再判敌清空(胜);停手与耗尽轮恒同图,清图轮才允许换图。 - 关卡收束:关卡结束清空魔法道具背包恰一次并返回清空前计数;战斗结束不清背包。 -- 可回放(封闭随机流口径):同种子同输入两遍步骤流逐位复现;随机消费仅限五处封闭随机流——开战掷骰(初始化随机)、技能摇号 `behavior_roll`(回合内、开轮时点、步骤流之外)、护甲等级掷点 `armor_roll`(回合内、步骤流之内)、奖励格摇号 `reward_roll`(初始化、装板期)、换板棋盘生成 `board_gen`(初始化、清图轮换板时点),无未登记随机源、零界面等待;单敌出招轨迹可独立复放、互不穿插。 +- 可回放(封闭随机流口径):同种子同输入两遍步骤流逐位复现;随机消费仅限六处封闭随机流——开战掷骰(初始化随机)、技能摇号 `behavior_roll`(回合内、开轮时点、步骤流之外)、护甲等级掷点 `armor_roll`(回合内、步骤流之内)、奖励格摇号 `reward_roll`(初始化、装板期)、变异布置 `variant_place`(初始化、装板与资源预分配之后)、换板棋盘生成 `board_gen`(初始化、清图轮换板时点),无未登记随机源、零界面等待;单敌出招轨迹可独立复放、互不穿插。 - 无来源即炸:每行明细都能指回一个来源事实(棋盘格、单位、清图或段级结论);找不到来源即断言失败,行型与来源类别两层豁免清单见明细流水一节,豁免行仍受其它契约约束。 - 行型封闭:流水只出现本文登记的 10 类行型;新增或下线行型必须走本文修订并同步呈现层行格式化,未登记行型不得落行。 diff --git a/design/gdd/systems/排雷循环/地形变异.md b/design/gdd/systems/排雷循环/地形变异.md index 6fbe16ae..29271c1d 100644 --- a/design/gdd/systems/排雷循环/地形变异.md +++ b/design/gdd/systems/排雷循环/地形变异.md @@ -1,131 +1,146 @@ # 地形变异 -> 一句话:格子的非雾变异(岩浆/冰冻等,待定) -> 状态:已定稿(自旧体系迁移) +> 一句话:格子的非雾变异(荆棘丛/藤蔓缠绕/岩浆/烬覆),挖掘中可遭遇的地貌偏离 +> 状态:已定稿(首批类型随 2026-09-19 定稿批落地) > 旧版参考:../../../archive/gdd-2026-09-14/a4-terrain-variation.md ## 设计目的 -地形变异是一个定义层的框架:它允许给棋盘上的格子挂上"变异"标记(比如岩浆、冰冻),让某片格子在基础扫雷规则之外多一层玩家在挖掘中可遭遇的偏离。没有这个框架,关卡之间的差异只能靠雷的数量单调缩放,"每关解锁新元素"的推进感和越深越险的地貌变化都无处安放。 +地形变异是一个定义层的框架:它允许给棋盘上的格子挂上"变异"标记,让某片格子在基础扫雷规则之外多一层玩家在挖掘中可遭遇的偏离。没有这个框架,关卡之间的差异只能靠雷的数量单调缩放,"每关解锁新元素"的推进感和越深越险的地貌变化都无处安放。 -变异带来的行为改变发生在数据侧:格子只是多携带一条数据,读取到它的系统按数据行动,规则本体一个分支都不加。首批变异类型目前只登记了名字(岩浆、冰冻),机制分化未定;本定稿只固化框架本体与配套约束,不为任何类型编造机制——这是自上一版设计压缩改写后的诚实现状。 +变异带来的行为改变发生在数据侧:格子只是多携带一条数据,读取到它的系统按数据行动,规则本体一个分支都不加。首批变异类型已按两关地形背景定稿(第一关平原/密林基线:荆棘丛 + 藤蔓缠绕;第二关幽暗地域:岩浆 + 烬覆),机制全部经由「效果 / 触发 / 布置约束」三枚举声明——新增类型 = 清单加行 + 必要时扩充枚举词表,不改读取方。 ## 系统范围 -边界这样划:地形变异只管"变异是什么、以什么形式存在、遵守什么纪律"。变异摆在哪、摆多大归棋盘生成;挖格、插旗、连锁展开怎么执行归排雷玩法。现行状态下它没有任何机制已定稿的变异内容,是一套"框架就位、内容待定"的系统。 +边界这样划:地形变异只管"变异是什么、以什么形式存在、遵守什么纪律"。变异怎么随战斗初始化布置归战斗结算的编排序;挖格、插旗、连锁展开怎么执行归排雷玩法。 负责: | 负责 | 说明 | |---|---| | 变异框架本体 | 变异类型清单的登记结构:要加一类变异,就在清单里加一行声明,不改代码 | -| 数据侧行为约定 | 变异是格子的只读数据,随图生成、随图消亡;读取方按数据行动 | +| 数据侧行为约定 | 变异是格子的只读数据,随图生灭;读取方按数据行动 | | 零特判原则 | 禁止任何系统为变异改写扫雷规则本体——框架的承重约束(见核心规则) | -| 布置接口结构位 | 变异在棋盘生成期布置;布置约束与参数待定稿,结构位保留 | -| 反制道具配套义务 | 未来每定稿一类变异,随批配至少一件反制道具(义务在本系统,道具本体归魔法道具) | +| 枚举抽象 | 每类变异以「效果(effect)/ 触发(trigger)/ 布置约束(placement)」三枚举声明机制;读取方按枚举路由,不按类型名分支 | +| 反制道具配套义务 | 每定稿一类变异,随批配至少一件反制道具(义务在本系统,道具本体归魔法道具;首批四类型的兑现改排 Alpha——2026-09-19 用户拍板) | 不负责: | 不负责 | 归谁 | |---|---| -| 布置算法与参数(摆几个、多大、摆哪) | [棋盘生成](棋盘生成.md) | +| 布置的编排时点与算法执行(变异格赋予在棋盘设置顺序第 4 步) | [战斗结算](../冒险循环/战斗结算.md)(编排段执行面);布置参数数值归[难度曲线与Boss](../冒险循环/难度曲线与Boss.md) | | 挖格、插旗、连锁展开的规则本体 | [排雷玩法](排雷玩法.md) | | 反制道具的效果、时效与定价 | [魔法道具](魔法道具.md) | | 数值表本体与读取机制 | [数据配置](../表现与支撑/数据配置.md) | -| 界面呈现 | [HUD 框架](../表现与支撑/HUD框架.md) | +| 界面呈现(数字遮蔽的渲染执行) | [HUD 框架](../表现与支撑/HUD框架.md) | | 教学口径 | [教学引导](../表现与支撑/教学引导.md) | ## 在核心循环中的位置 ``` -棋盘生成 ──(生成期布置:格子携带变异数据)──▶ 扫雷阶段:读取方按数据行动 +棋盘生成 ──▶ 打开状态初始化(初始展开)──▶ 地图战斗资源格设置 ──▶ 变异格设置 ──▶ 挖掘阶段:读取方按数据行动 ``` -地形变异站在棋盘生成与排雷玩法之间:棋盘生成在生成期决定哪片格子带变异,玩家在挖掘中遭遇它。它自己不下达任何指令——挖格、插旗、连锁展开仍按排雷玩法的规则执行,变异的全部影响都经由格子数据产生。 +棋盘设置顺序(2026-09-19 用户拍板):生成棋盘 → 棋盘打开状态的初始化 → 战斗资源格的设置 → 变异格的设置。变异格赋予在最后一步,天然不落初始展开已开区——伤害型「只出现在未打开的格子」的布置约束由该顺序结构保障,不靠生成器回溯排除。变异自己不下达任何指令——挖格、插旗、连锁展开仍按排雷玩法的规则执行,变异的全部影响都经由格子数据产生。 ## 核心规则 -本节只写现行存活的内容,即变异框架本体;首批变异类型机制定稿前,以下规则不含任何具体类型的玩法行为。 - ### 变异框架与类型清单 -每种变异是配置表里的一行具名声明。框架自身不含任何运行时逻辑——变异带来的行为改变,发生在"读取方读到什么数据、按什么约定行动"的交集里;定义行只声明数据,不写处理代码。要加一类变异,就在清单里加一行声明,而不是改代码。 +每种变异是配置表里的一行具名声明。框架自身不含任何运行时逻辑——变异带来的行为改变,发生在"读取方读到什么数据、按什么约定行动"的交集里。要加一类变异,就在清单里加一行声明,而不是改代码;读取方(战斗结算 / 呈现层)按三枚举路由,不按类型名分支。 -变异的生命周期是随图生灭:变异是棋盘格的只读属性,在棋盘生成期布置、随棋盘实例存在;清图换图或战斗结束时随图消亡。生效期没有任何运行时写入,也没有跨图存续——新图重新布置。 +变异的生命周期是随图生灭:变异是棋盘格的只读属性,在战斗初始化末段布置、随棋盘实例存在;清图换图或战斗结束时随图消亡。布置之后生效期没有任何运行时写入,也没有跨图存续——新图重新布置。 -清单现行登记两个类型名,都只有名字: +清单现行登记四个类型,两轴 × 两关: -| 类型名 | 现状 | -|---|---| -| 岩浆 | 仅登记名字,机制未定(provisional;出处为游戏概念文档的存疑项登记) | -| 冰冻 | 仅登记名字,机制未定(provisional;出处同上) | +| 类型名 | 效果 | 触发 | 布置约束 | 解锁 | 地形 | 伤害量 | +|---|---|---|---|---|---|---| +| 荆棘丛 | 直伤(flat_damage) | 被打开即触发(on_open) | 只落未开格(closed_only) | 第 1 关 | dungeon | 1 | +| 藤蔓缠绕 | 数字遮蔽(hide_number) | 打开且显示数字时(on_number_shown) | 任意格(any) | 第 1 关 | dungeon | — | +| 岩浆 | 直伤(flat_damage) | 被打开即触发(on_open) | 只落未开格(closed_only) | 第 2 关 | underdark | 2 | +| 烬覆 | 数字遮蔽(hide_number) | 打开且显示数字时(on_number_shown) | 任意格(any) | 第 2 关 | underdark | — | -清单结构在归档期曾有完整字段定义,共八个:标识、玩家可见名、解锁关号、作用域、信息规则、布置约束、地形主题、反制道具清单。其中"信息规则"与"布置约束"两个字段的取值语义绑定具体变异类型,字段集是否沿用随首批类型定稿一并拍板(历史登记见调参表指向的 registry 条目)。 +机制口径(2026-09-19 定稿批): + +- 伤害型:挖开变异格(含连锁揭示,chain 通道同口径)在轮末结算逐行兑现扣生命——地形后果同挖雷口径,非奖励、插旗格直挖不作废;雷格恒不吃变异(双层叠罚污染逐行归因,首版排除)。 +- 信息型:已开且显示数字时该格真实数字不呈现(呈现层读取遮蔽;挖掘规划被迫降级为盲区推理)。呈现层读取不回灌规则本体——模拟试玩走查询面、不经遮蔽,读数口径不受影响。 +- 直伤量为加法核小整数(effect_value ∈ [1,9],行值 provisional 归难度曲线与Boss);每格携带单类型单 id。 +- 变异格未开时无预告呈现(遭遇式;视觉标识归表现层升级批)。 + +候选池门控(框架级现行机制):一种变异要进入本关的布置候选,须同时满足两个条件——它的解锁关号不大于当前关卡序号(直接整数比较),且它声明的地形(board_types)与本地形有交集(声明为空集等于通配)。候选池为空是合法状态:静默布置一张无变异的普通图,不报错、不警告。门控谓词与取值域登记于 [registry 的 variant_pool_gate 条目](../../../registry/entities.yaml)。 ### 零特判原则(承重约束) 变异不得改写扫雷规则本体:挖格、插旗、连锁展开的判定与执行不增加任何变异分支,代码里不允许出现"这格带某变异所以就……"。这是框架的承重约束——它保证每加一类变异都不需要回头修改规则本体,玩家面对的也始终是同一套扫雷规则,而不是一套到处开洞的例外。 -交互全部走数据侧:规则本体只读取格子数据并按数据行动,变异的全部影响经由数据与读取方之间的约定产生。若未来某类变异的机制确实需要规则本体做出区分,那是一次同时修订排雷玩法与本文的正式变更,不是框架默认允许的能力。 +交互全部走数据侧:规则本体只读取格子数据并按数据行动,变异的全部影响经由数据与读取方之间的约定产生(伤害型由战斗结算兑付、信息型由呈现层遮蔽)。若未来某类变异的机制确实需要规则本体做出区分,那是一次同时修订排雷玩法与本文的正式变更,不是框架默认允许的能力。 -### 生成期布置接口(结构位) +### 布置段(战斗初始化第 4 步) -变异在棋盘生成阶段布置:生成新雷图时决定本图是否携带变异、带在哪些格子上,玩家在挖掘中遭遇。本系统只声明"变异在生成期布置"这个接口位置;布置约束(变异允许落在什么格子上)与布置参数(摆几个、多大、摆哪)未定稿,将与首批变异类型的机制一起拍板,归属棋盘生成的管线预留段。定稿前,棋盘生成不为变异做任何额外工作。 +变异在战斗初始化末段布置:初始展开完成、地图战斗资源格设置之后,由战斗结算的编排段执行。候选池非空时从池内均匀选一类型,按布置参数生长连通片写入格子快照: -布置之前有一步候选池门控,它是框架级的现行机制:一种变异要进入本关的布置候选,须同时满足两个条件——它的解锁关号不大于当前关卡序号(直接整数比较,不做舍入换算),且它声明的地形主题与本地形主题有交集(主题声明为空集等于通配,任何地形都算匹配)。候选池为空是合法状态:生成器静默布置一张无变异的普通图,不报错、不警告。门控谓词与取值域登记于 [registry 的 variant_pool_gate 条目](../../../registry/entities.yaml)。 +- 数量 = round(总格数 × percent_of_cells / 100) + (关卡序号 − 1) × per_level_bonus(当前 5% + 每关 +2——图规模百分比兼顾大小图,关卡递增承载"越深越险";provisional 归难度曲线与Boss)。 +- 形状 = 全连通生长(8 邻),单片 ≤ pack_max(当前 4:2×2 / 1×4 / 1×3+1 等形;2026-09-19 用户拍板)。 +- 候选格域 = 安全格 − 初始展开区,且排除雷格。 +- 布置取数走专用随机流(variant_place,一板恰一 scope),换图/轮内刷新图各取号一次,确定性可复放。 +- 生成器管线不布置变异(产物恒零变异格)——布置域的"未开"语义由设置顺序保障。 ### 反制道具配套义务 -未来每定稿一类变异,必须随批配套至少一件反制道具(针对该变异、给玩家提供应对手段的道具),不让任何变异成为只亏不赚的纯税。这条配套义务的归属方是本系统;"反制"作为道具的一个维度,由魔法道具的五维度框架承接,反制道具的具体效果、时效与定价都归魔法道具定稿。 +每定稿一类变异,必须随批配套至少一件反制道具(针对该变异、给玩家提供应对手段的道具),不让任何变异成为只亏不赚的纯税。这条配套义务的归属方是本系统;"反制"作为道具的一个维度,由魔法道具的五维度框架承接,反制道具的具体效果、时效与定价都归魔法道具定稿。**首批四类型的反制兑现改排 Alpha**(2026-09-19 用户拍板:反制/秘宝类设计集中在 Alpha——垂直切片先把基础循环流程走完;切片期变异暂无反制手段的纯税状态已知悉接受),类型行的 counter_items 字段位已预留。 ## 依赖与输出 -依赖面很窄:上游只有数据承载与布置结构位,下游是对读取方与配套道具的义务声明。这张表同时是删除概念时的影响面清单。 +依赖面很窄:上游只有数据承载与布置执行位,下游是对读取方与配套道具的义务声明。这张表同时是删除概念时的影响面清单。 | 方向 | 系统 | 接口要点 | |---|---|---| | 上游 | [数据配置](../表现与支撑/数据配置.md) | 变异类型清单的表结构承载与统一读取;本系统是清单内容的回填责任方 | -| 上游 | [棋盘生成](棋盘生成.md) | 生成期布置的结构位(管线预留段);布置约束与参数待定稿 | +| 上游 | [战斗结算](../冒险循环/战斗结算.md) | 布置段的编排执行位(棋盘设置顺序第 4 步)+ 伤害型兑付面(轮末结算逐行兑现) | | 下游 | [排雷玩法](排雷玩法.md) | 零特判义务的承接方:执行逻辑不按变异分支,只按格子数据行动 | -| 下游 | [魔法道具](魔法道具.md) | 反制道具配套义务的承接方(五维度框架的"反制"维度) | -| 相邻 | [行动经济](行动经济.md) | "地形成本修饰"价目钩子(不同格子不同价)暂缓启用,待本系统首批变异定稿后重议(见该文档 P1/P2 后置) | +| 下游 | [魔法道具](魔法道具.md) | 反制道具配套义务的承接方(五维度框架的"反制"维度;首批兑现排 Alpha) | +| 相邻 | [行动经济](行动经济.md) | "地形成本修饰"价目钩子(不同格子不同价)暂缓启用,待变异类型扩展(见该文档 P1/P2 后置) | ## P0 最小闭环 -P0 阶段本系统不产出任何可玩内容:变异类型清单为空,没有机制已定稿的变异,所有雷图都是无变异的普通图。P0 只承诺一件事——框架就位且惰性: +首批类型已定稿,P0 空清单口径降为候选池空白的合法态描述: -``` -变异类型清单为空 ──▶ 棋盘生成按普通图出图 ──▶ 挖掘全程读不到变异数据 -``` - -1. 清单为空是合法状态:配置加载零错误、零警告,游戏行为与不存在本系统时完全一致。 -2. 框架就位指清单的登记结构与读取通道可用,向清单加入一行类型即应生效、不需要改动代码;首批类型定稿前不建行、不写任何变异处理代码。 +1. 候选池为空是合法状态(如门控双闸都筛空):配置加载零错误、零警告,该图行为与不存在本系统时完全一致。 +2. 框架就位指清单的登记结构与读取通道可用,向清单加入一行类型即应生效、不需要改动代码(枚举词表内)。 ## P1 / P2 后置 | 优先级 | 内容 | 后置原因 | |---|---|---| -| P1 | 首批变异类型(岩浆/冰冻)机制定稿并建行 | 名字已登记、机制分化未定;定稿批随批配至少一件反制道具 | -| P1 | 全局作用域变异(精英/Boss 图整图生效的变异形态) | 作用域语义未定稿,归[难度曲线与Boss](../冒险循环/难度曲线与Boss.md) | -| P2 | "地形成本修饰"价目钩子启用 | 由行动经济持有;启用须随首批变异定稿,与行动经济重议按格定价的接口 | +| Alpha | 首批四类型的反制道具兑现(counter_items 回填) | 2026-09-19 用户拍板:反制/秘宝类设计集中 Alpha;切片期先把基础循环走完 | +| Alpha | 全局作用域变异(精英/Boss 图整图生效的变异形态) | Boss 图变异暂无设计想法——2026-09-19 用户拍板留 Alpha 再设计;作用域语义归[难度曲线与Boss](../冒险循环/难度曲线与Boss.md) | +| P2 | "地形成本修饰"价目钩子启用 | 由行动经济持有;启用须随变异类型扩展,与行动经济重议按格定价的接口 | | P2 | 变异解锁节奏复核 | "每关至少引入一个新元素"的推进节奏,随[难度曲线与Boss](../冒险循环/难度曲线与Boss.md)定稿复核 | +| P2 | 变异格视觉标识与预告呈现 | 首批为遭遇式(未开无预告);标识位随表现层升级批 | ## 调参表 -本系统现行没有可调参数,变异表是空清单。归档文档曾为变异表登记过参数与行内容(如首类变异的解锁关号、地形主题标签),它们未随本定稿迁移;是否以新变异类型重定、如何重定,随首批类型定稿批一并拍板。 +| 参数 | 现行值 | 归属 | +|---|---|---| +| 布置数量基数 percent_of_cells | 5(总格数百分比) | 难度曲线与Boss(provisional) | +| 关卡递增 per_level_bonus | +2 格/关 | 难度曲线与Boss(provisional) | +| 连通片上限 pack_max | 4 | 定稿值(2026-09-19 用户拍板形状约束) | +| 直伤量 effect_value | 荆棘丛 1 / 岩浆 2 | 难度曲线与Boss(provisional,加法核 [1,9]) | -变异表的结构历史(字段定义、条目归属)见 [registry 的 variation_table 条目](../../../registry/entities.yaml)。该条目现行字段定义沿用归档期口径且无反制道具回填值(空清单无引用行),本系统首批类型定稿后应随该批重定内容。 +变异表的结构历史(字段定义、条目归属)见 [registry 的 variation_table 条目](../../../registry/entities.yaml)。字段集现行口径:标识 / 玩家可见名 / 解锁关号 / 作用域 / 效果 / 效果量 / 触发 / 布置约束 / 地形(board_types)/ 反制道具绑定(counter_items,Alpha 回填)。 ## 验收标准 -- 空清单合法:变异类型清单为空(即现行状态)时,配置加载零错误、零警告,游戏行为与无本系统时完全一致。 -- 零特判(负验收):排雷玩法的挖格、插旗、连锁展开执行路径中不存在任何按变异类型分支的代码;本系统自身代码零玩法数值字面量、零随机数调用。 -- 数据生命周期:格子携带的变异是只读数据——随图生成、随图消亡,生效期零运行时写入,无跨图存续。 -- 配套义务闭环:任何一类变异的定稿批,若不含至少一件反制道具的定稿条目,验收不通过。 +- 候选池空合法:门控筛空(池空或候选域碎片化不足)时静默零布置,配置加载零错误、零警告。 +- 零特判(负验收):排雷玩法的挖格、插旗、连锁展开执行路径中不存在任何按变异类型分支的代码;读取方按 effect/trigger/placement 枚举路由。 +- 数据生命周期:格子携带的变异是只读数据——战斗初始化末段布置、随图消亡,布置后零运行时写入,无跨图存续;生成器产物恒零变异格。 +- 布置域:变异格全在初始展开区外(伤害型 closed_only 由设置顺序结构保障)且不吃雷格;每图至多一种类型;连通片 ≤ pack_max。 +- 配套义务闭环:任何一类变异的定稿批,若不含反制兑现条目(当批兑现或显式改排 Alpha 登记),验收不通过——首批四类型已按改排 Alpha 登记。 ## 版本历史 | 版本 | 日期 | 改动 | |---|---|---| | v0.1 | 2026-09-15 | 自归档 GDD 压缩改写 | -| v0.2 | 2026-09-15 | 雾格残留清理:删除上一版设计遗留的「待拍板」失义条目十条(雾区布置约束族、承重数学、雾占比上限、剔除雾格的双向净效应建模、清图预算雾增量、雾板两态验算与护栏、雾板定价硬约束、雾呈现义务、雾教学条目、变异表反制道具回填值)与文首说明;调参表删除指向该节的指引。变异框架本体(清单加行即生效、零特判、随图只读生命周期、候选池门控、反制道具配套义务)不变 | +| v0.2 | 2026-09-15 | 雾格残留清理:删除上一版设计遗留的「待拍板」失义条目十条;变异框架本体不变 | +| v0.3 | 2026-09-20 | 首批类型定稿批:四类型两轴×两关(荆棘丛/藤蔓缠绕/岩浆/烬覆)落地;机制枚举化(effect/trigger/placement 三枚举 + 跨字段互锁);布置段改排战斗初始化第 4 步(2026-09-19 用户拍板棋盘设置顺序);biome 系命名退役对齐 board_type 词表;反制兑现与全局作用域改排 Alpha;岩浆/冰冻旧占位名退役(冰冻退役、岩浆落位第二关) | diff --git a/design/gdd/systems/排雷循环/棋盘生成.md b/design/gdd/systems/排雷循环/棋盘生成.md index 880c435f..e8fd720e 100644 --- a/design/gdd/systems/排雷循环/棋盘生成.md +++ b/design/gdd/systems/排雷循环/棋盘生成.md @@ -10,16 +10,16 @@ ## 系统范围 -边界这样划:本系统管"图从哪来、生成时承诺什么质量"。图上的动作怎么执行归[排雷玩法](排雷玩法.md),什么时候换图归[战斗结算](../战斗循环/战斗结算.md)编排,数值定多少归[难度曲线与Boss](../冒险循环/难度曲线与Boss.md)(本系统只提供参数表给数值落点),变异内容是什么归[地形变异](地形变异.md)(本系统只留结构位,并在生成期执行对方的约束)。本系统是个无状态的生产者:进参数与种子,出棋盘与指标,自己不留任何记忆。 +边界这样划:本系统管"图从哪来、生成时承诺什么质量"。图上的动作怎么执行归[排雷玩法](排雷玩法.md),什么时候换图归[战斗结算](../战斗循环/战斗结算.md)编排,数值定多少归[难度曲线与Boss](../冒险循环/难度曲线与Boss.md)(本系统只提供参数表给数值落点),变异内容是什么归[地形变异](地形变异.md)(本系统不为变异做任何工作——变异格赋予在战斗初始化第 4 步执行,生成器产物恒零变异格)。本系统是个无状态的生产者:进参数与种子,出棋盘与指标,自己不留任何记忆。 负责: | 负责 | 说明 | |---|---| -| 五阶段确定性生成管线 | 读参数 → 布雷 → 变异布置 → 生成-验证 → 产出上报;顺序冻结,不调换、不跳步 | +| 四阶段确定性生成管线 | 读参数 → 布雷 → 生成-验证 → 产出上报;顺序冻结,不调换、不跳步(变异布置段 2026-09-20 随棋盘设置顺序拍板移出管线) | | 棋盘生成参数表 | 全项目第一张按进度键(关卡序号 × 节点深度)回填的配置表;表结构归本系统定 | | 可推理率度量与达标判定 | 生成时刻的质量承诺,含计算口径与定义版本号 | -| 出生区与反退化保证 | 出生 3×3 无雷无变异、最小安全格数、开局白给板拒绝、清图可达性 | +| 出生区与反退化保证 | 出生 3×3 无雷、最小安全格数、开局白给板拒绝、清图可达性(变异不落出生区与展开域由布置段顺序保障) | | 加载期参数校验 | 参数域、布雷数下限、可行性与成本闸校验;违例加载期致命 | | 生成期指标集产出 | 实测可推理率、实际雷数等,供调参与下游对账 | @@ -30,7 +30,7 @@ | 挖掘、插旗、连锁展开的执行 | [排雷玩法](排雷玩法.md) | | 换图时机与轮次编排 | [战斗结算](../战斗循环/战斗结算.md)(本系统纯被调用) | | 难度数值定多少 | [难度曲线与Boss](../冒险循环/难度曲线与Boss.md)(调参所有权在对方) | -| 变异内容与解锁节奏 | [地形变异](地形变异.md)(本系统只消费其候选池门控) | +| 变异内容与解锁节奏 | [地形变异](地形变异.md)(布置执行位在其自身——战斗初始化第 4 步,不在本管线) | | 奖励内容与图类型差异化插槽 | [地图战斗资源](../战斗循环/地图战斗资源.md) | | 界面呈现 | [HUD 框架](../表现与支撑/HUD框架.md) | | 配置表承载与读取机制 | [数据配置](../表现与支撑/数据配置.md)(本系统是该表的回填责任方) | @@ -45,9 +45,9 @@ ## 核心规则 -### 五阶段管线:一次调用跑完五步,同种子必产同图 +### 四阶段管线:一次调用跑完四步,同种子必产同图 -每次生成都是一次纯粹的函数调用——输入关卡序号、节点深度和随机种子,输出一张完整棋盘加一组生成期指标。调用按固定五步跑完,顺序冻结: +每次生成都是一次纯粹的函数调用——输入关卡序号、节点深度和随机种子,输出一张完整棋盘加一组生成期指标。调用按固定四步跑完,顺序冻结: ``` 生成(关卡序号, 节点深度, 种子) → 棋盘 + 生成期指标集 @@ -57,14 +57,12 @@ │ 在"全格 − 出生 3×3"范围均匀布雷 │ 开局免费展开域外扩一圈,圈内的雷移除(只减不增、不重投、无迭代) ▼ - ③ 变异布置 ─── 消费地形变异的候选池门控;池空或约束无解 → 静默跳过,产出无变异图 + ③ 生成-验证 ── 实测可推理率;不达标在同一张图的随机数配额内重试,至上限按出口分发 ▼ - ④ 生成-验证 ── 实测可推理率;不达标在同一张图的随机数配额内重试,至上限按出口分发 - ▼ - ⑤ 产出上报 ─── 交出棋盘 + 生成期指标集 + ④ 产出上报 ─── 交出棋盘 + 生成期指标集 ``` -生成期间的每一次随机取数——布雷、变异、重试——都从本图专属的一条"棋盘生成随机流"按固定顺序取:一张图只开一条流、用到底,重试不开新流;流之外零随机,全局随机函数一律禁用。参数行开始时一次快照,配置表中途热重载不影响进行中的生成。管线本身无状态——这些合起来就是"同种子同参数必产同图"的根基。 +生成期间的每一次随机取数——布雷、重试——都从本图专属的一条"棋盘生成随机流"按固定顺序取:一张图只开一条流、用到底,重试不开新流;流之外零随机,全局随机函数一律禁用。参数行开始时一次快照,配置表中途热重载不影响进行中的生成。管线本身无状态——这些合起来就是"同种子同参数必产同图"的根基。(变异布置不消费本流——其取数走专用 variant_place 流,归战斗结算 布置段。) 各阶段的失败都有明确语义,全部失败路径都不让玩家卡死(本节为纯流程规则,无计算;数值与校验式见后续各节): @@ -72,13 +70,12 @@ |---|---|---| | 参数解析 | 行缺失或域违例 | 加载期校验已拦,运行期不会失败 | | 布雷 | 雷数为 0 | 不会发生(零雷板加载期已拒) | -| 变异布置 | 布置约束无解 | 静默跳过变异,产物合法,实际达成数随指标上报 | | 生成-验证 | 重试耗尽 | 按三分支出图(见"生成重试与成本闸") | | 产出 | —— | 无失败路径,交出棋盘与指标 | ### 出生区安全与反退化布雷:脚下无雷、废板不出厂 -玩家开局站在棋盘上的 3×3 出生区,开局不花行动点就能看到一片区域——出生 3×3 加上从它出发经连锁展开(翻到八邻无雷的格时整片免费翻开的机制,玩家可见名"坍塌显形")免费带出的格,下文称**开局免费展开域**。本规则保证两件事:其一,脚下永远安全——出生区与开局免费展开域内恒无雷、恒无变异;其二,流出的每张图都可正常游玩——零雷板、安全格寥寥的板、开局白给的板、带孤岛安全格的板,都在生成期被拦下或整张弃用。 +玩家开局站在棋盘上的 3×3 出生区,开局不花行动点就能看到一片区域——出生 3×3 加上从它出发经连锁展开(翻到八邻无雷的格时整片免费翻开的机制,玩家可见名"坍塌显形")免费带出的格,下文称**开局免费展开域**。本规则保证两件事:其一,脚下永远安全——出生区与开局免费展开域内恒无雷(变异亦然——变异格赋予在初始展开之后执行,天然不落已开区,见[地形变异](地形变异.md)布置段);其二,流出的每张图都可正常游玩——零雷板、安全格寥寥的板、开局白给的板、带孤岛安全格的板,都在生成期被拦下或整张弃用。 **结论**:雷数先定数、再落位;零雷板与安全格不足的参数组合在加载期就被拒绝。 @@ -99,7 +96,7 @@ | 反退化规则 | 内容 | |---|---| | 先布后删 | 先把 N 颗雷在"全格 − 出生 3×3"上布完,再把开局免费展开域外扩一圈内的雷移除——只减不增、不重投、无迭代,必然终止且可复现;实际雷数与 N 的差额随指标观测 | -| 出生区安全 | 出生 3×3 与开局免费展开域内恒无雷、恒无变异;外扩一圈内禁变异、可有雷 | +| 出生区安全 | 出生 3×3 与开局免费展开域内恒无雷;变异不落两域由布置段顺序保障(非本管线义务) | | 开局白给板拒绝 | 开局免费展开域不得覆盖全部安全格——等于即退化板,尝试弃用重试;运行期另有同义兜底不变式,归[排雷玩法](排雷玩法.md) | | 清图可达性 | 每个安全格都必须能从出生区沿安全格八向连通走到;带孤岛安全格的板判非法,不进降级候选 | @@ -166,7 +163,7 @@ | 方向 | 系统 | 接口要点 | |---|---|---| | 上游 | [数据配置](../表现与支撑/数据配置.md) | 参数表承载、类型化读取与加载期校验通道;棋盘生成随机流的种子派生(每图开一次流) | -| 上游 | [地形变异](地形变异.md) | 变异候选池门控:本系统消费其约束并在生成期强制执行(MVP 内容待定,恒空池) | +| 相邻 | [地形变异](地形变异.md) | 相邻事实依赖:布置执行位在战斗初始化第 4 步(战斗结算 编排段),生成器产物恒零变异格;出生区/展开域"无变异"承诺由该顺序结构保障 | | 下游 | [排雷玩法](排雷玩法.md) | 棋盘产物消费方;开局免费展开由对方执行,展开域口径按双方登记的回验条款对齐 | | 下游 | [战斗结算](../战斗循环/战斗结算.md) | 换图调用方与指标对账方;零合法板的玩家侧诚实告知由其编排 | | 下游 | [行动经济](行动经济.md) | 单向事实依赖:P(安全)(挖掘目标格为安全格的概率)是对方挖掘风险不等式的概率锚,锚值由本系统供给——实测可推理率分布的低分位值,不是及格线参数 | @@ -186,9 +183,8 @@ 1. 战斗结算以(关卡序号,节点深度)调用生成,本系统开一条棋盘生成随机流。 2. 读取并快照参数行(加载期校验已通过)。 3. 先定数布雷、先布后删;出生区安全与反退化不变式全过。 -4. 变异布置消费候选池门控;候选池为空则产出无变异图。 -5. 实测可推理率,不达标在流内重试至上限,按三分支出图。 -6. 交出棋盘 + 指标集:排雷玩法消费棋盘,战斗结算消费指标对账——循环闭合于下一次换图请求。 +4. 实测可推理率,不达标在流内重试至上限,按三分支出图(变异布置不在本管线——装板后由战斗结算 布置段执行)。 +5. 交出棋盘 + 指标集:排雷玩法消费棋盘,战斗结算消费指标对账——循环闭合于下一次换图请求。 ## P1 / P2 后置 @@ -199,7 +195,7 @@ | P1 | 与排雷玩法的展开域回验集成 | 回验两臂登记在对方文档,集成随对方排期 | | P2 | 推理域扩展(solver_rules 启用其他规则集或新增信息源) | 改可推理率定义 = 文档修订,MVP 冻结 | | P2 | 生成异步化与成本闸正式值定稿 | 实现期决策,随性能预算批 | -| P2 | 变异布置内容接入(岩浆/冰冻类地形变异) | 结构位已预留;内容与解锁归地形变异定稿 | +| P2 | ~~变异布置内容接入~~(已收口——2026-09-20 首批类型定稿批:布置执行位改排战斗初始化第 4 步,管线零布置义务化) | — | ## 调参表 @@ -220,7 +216,7 @@ ## 验收标准 - 确定性复现:同参数行同种子生成两遍,格状态与指标集逐位相同;换种子产物不全同。 -- 出生区安全:出生 3×3 与开局免费展开域内零雷零变异;外扩一圈内零变异(可有雷)。 +- 出生区安全:出生 3×3 与开局免费展开域内零雷(零变异由布置段顺序保障——生成器产物恒零变异格)。 - 数字恒真:每个非雷格的数字恒等于其八邻雷数。 - 布雷纪律:N ≥ 1、实际雷数 ≤ N、实际安全格数 ≥ min_safe_count。 - 无开局白给板:开局免费展开域 ≠ 全部安全格,违例尝试弃用重试。 diff --git a/design/gdd/systems/排雷循环/魔法道具.md b/design/gdd/systems/排雷循环/魔法道具.md index 8d90d1f7..462e8cbc 100644 --- a/design/gdd/systems/排雷循环/魔法道具.md +++ b/design/gdd/systems/排雷循环/魔法道具.md @@ -138,7 +138,7 @@ | P1 | 道具使用的动效与音频反馈 | 表现打磨项后置;音频排 Alpha | | P1 | "买情报"闭环教学与"真视不是保护"口径 | 归教学引导,Alpha 里程碑,非阻塞 | | P1 | 信息类道具的战斗当量入战斗结算效果对照表 | 间接效应的定量换算,调参期扩列,MVP 不入表 | -| P2 | 地形变异新类型(岩浆/冰冻等)的反制道具 | 随变异建行批,按框架契约每变异至少一件 | +| P2 | 地形变异新类型的反制道具 | 归 Alpha 反制批(2026-09-20 用户拍板)——按框架契约每变异至少一件;首批四类型(荆棘丛/藤蔓缠绕/岩浆/烬覆)已建行,counter_items 待回填 | ## 调参表 diff --git a/design/registry/entities.yaml b/design/registry/entities.yaml index 8b1bd8bb..dd0c02cd 100644 --- a/design/registry/entities.yaml +++ b/design/registry/entities.yaml @@ -20,10 +20,10 @@ entities: owner: design/gdd/systems/表现与支撑/数据配置.md attributes: kind: autoload - streams: [board_gen, reward_roll, encounter, armor_roll, behavior_roll] - stream_names: 当前封闭清单(增流需改代码 + P1 支柱评审;combat_var 已于 2026-09-07 full 评审删除——玩法随机不预置基础设施;encounter 增流 2026-09-12 用户拍板——遭遇随机化批,scope_event_binding = 每次战斗实例化恰一 scope;**armor_roll 增流 2026-09-16 用户拍板**——护甲等级掷点批,回合内随机首次入流:scope_event_binding = 每次掷点恰一 scope〔逐次掷逐次取号、可逐位回放〕,消费面 = 战斗结算 吃掷点分量判定;**behavior_roll 增流 2026-09-16 用户拍板**——敌人技能池批(开轮摇号):回合内随机、时点前置到每轮开轮〔结算步骤流之外〕,scope_event_binding = 每敌每摇号恰一 scope,**派生向量 = spawn_index × 1000000 + 单敌摇号序**〔单敌出招轨迹可独立复放、敌互不穿插;取号门 = open_scope(stream, scope) 显式 scope 取号——计数器仍单调 +1 供观测〕,消费面 = 战斗结算 开轮摇号换算层 + 技能摇号器 SkillLottery) + streams: [board_gen, reward_roll, encounter, armor_roll, behavior_roll, variant_place] + stream_names: 当前封闭清单(增流需改代码 + P1 支柱评审;combat_var 已于 2026-09-07 full 评审删除——玩法随机不预置基础设施;encounter 增流 2026-09-12 用户拍板——遭遇随机化批,scope_event_binding = 每次战斗实例化恰一 scope;**armor_roll 增流 2026-09-16 用户拍板**——护甲等级掷点批,回合内随机首次入流:scope_event_binding = 每次掷点恰一 scope〔逐次掷逐次取号、可逐位回放〕,消费面 = 战斗结算 吃掷点分量判定;**behavior_roll 增流 2026-09-16 用户拍板**——敌人技能池批(开轮摇号):回合内随机、时点前置到每轮开轮〔结算步骤流之外〕,scope_event_binding = 每敌每摇号恰一 scope,**派生向量 = spawn_index × 1000000 + 单敌摇号序**〔单敌出招轨迹可独立复放、敌互不穿插;取号门 = open_scope(stream, scope) 显式 scope 取号——计数器仍单调 +1 供观测〕,消费面 = 战斗结算 开轮摇号换算层 + 技能摇号器 SkillLottery;**variant_place 增流 2026-09-20 用户拍板**——地形变异首批类型定稿批:scope_event_binding = 每次装板恰一 scope〔常规换图/轮内刷新图各取号一次〕,消费面 = 战斗结算 布置段 _place_variants——变异类型选取 + 连通片生长取数) stream_facade: open_stream(stream) 取号(计数器+1→派生→返回已播种实例);derive_at(stream, scope) 纯函数供测试/预览(不推计数器) - scope_event_binding: board_gen=每次生成新扫雷图(棋盘生成);reward_roll=每次 board install(格奖励批 + 清图奖励批)(地图战斗资源——2026-09-09 接口漂移改写,原"每次奖励结算 roll 批次";地图战斗资源 Rule 2/AC-1 为契约源);encounter=每次战斗实例化恰一 scope(战斗结算 begin_battle 实例化期、扫雷开始前——unit_count 区间均匀骰,挖掘决策域之外 P1 合规);armor_roll=每次掷点恰一 scope(战斗结算 战斗段吃掷点分量逐次判定——1d20,护甲等级掷点批 2026-09-16) + scope_event_binding: board_gen=每次生成新扫雷图(棋盘生成);reward_roll=每次 board install(格奖励批 + 清图奖励批)(地图战斗资源——2026-09-09 接口漂移改写,原"每次奖励结算 roll 批次";地图战斗资源 Rule 2/AC-1 为契约源);encounter=每次战斗实例化恰一 scope(战斗结算 begin_battle 实例化期、扫雷开始前——unit_count 区间均匀骰,挖掘决策域之外 P1 合规);armor_roll=每次掷点恰一 scope(战斗结算 战斗段吃掷点分量逐次判定——1d20,护甲等级掷点批 2026-09-16);variant_place=每次装板恰一 scope(战斗结算 布置段——地形变异首批类型定稿批 2026-09-20) unseeded_access: 致命中止 snapshot_contract: 续玩/复盘快照 = master_seed + 各流 scope 计数器(消费方 存档序列化) referenced_by: [棋盘生成, 战斗结算, 地图战斗资源, 冒险结构, 存档序列化] @@ -34,17 +34,18 @@ entities: attributes: variables: [SALT_s, master_seed, scope] output_range: "[0, 2^63)" - golden_vectors: 见《数据配置》Formulas-R2 向量表(7 常量:board_gen m=42 scope0-3 · reward_roll m=42 scope0=7422916049646135537 · encounter m=42 scope0=1061841193803499209 · armor_roll m=42 scope0=4806095330042243845——2026-09-16 护甲等级掷点批 armor_roll 增流补向量 · behavior_roll m=42 scope0=426657203655691839——2026-09-16 敌人技能池批 behavior_roll 增流补向量) + golden_vectors: 见《数据配置》Formulas-R2 向量表(7 常量:board_gen m=42 scope0-3 · reward_roll m=42 scope0=7422916049646135537 · encounter m=42 scope0=1061841193803499209 · armor_roll m=42 scope0=4806095330042243845——2026-09-16 护甲等级掷点批 armor_roll 增流补向量 · behavior_roll m=42 scope0=426657203655691839——2026-09-16 敌人技能池批 behavior_roll 增流补向量 · variant_place m=42 scope0=1936946134361402387——2026-09-20 地形变异首批类型定稿批 variant_place 增流补向量) salt_board_gen: 0xA0761D6478BD642F salt_reward_roll: 0xE7037ED1A0B428DB salt_encounter: 0x8EBC6AF09C88C6E3(2026-09-14 encounter 增流——互异奇常数 ≠ 乘数) salt_armor_roll: 0xC2B2AE3D27D4EB2F(2026-09-16 armor_roll 增流——互异奇常数 ≠ 乘数) salt_behavior_roll: 0x85EBCA7758C24E15(2026-09-16 behavior_roll 增流——互异奇常数 ≠ 乘数) + salt_variant_place: 0x9AE16A3F2FAD6C39(2026-09-20 variant_place 增流——互异奇常数 ≠ 乘数) multiplier: 0x9E3779B97F4A7C15 impl_spec: wrap64=二补数自然回绕;禁 float 中间量、禁对中间量用 %、禁"主动修复溢出";常量用分段表达式书写;掩码收尾 reproducibility_claim: 同冒险内同流各 scope 无碰撞 + 跨版本恒定(不做跨 master_seed 全局单射声明) master_seed_int64_min: 致命(abs 恒等回绕→与 0 静默别名) - set_master_seed_precondition: 仅全流计数器全零时受理(5 流——2026-09-14 数据配置 漂移批口径 + 2026-09-16 护甲等级掷点批 armor_roll 增流 + 同日敌人技能池批 behavior_roll 增流) + set_master_seed_precondition: 仅全流计数器全零时受理(6 流——2026-09-14 数据配置 漂移批口径 + 2026-09-16 护甲等级掷点批 armor_roll 增流 + 同日敌人技能池批 behavior_roll 增流 + 2026-09-20 地形变异首批类型定稿批 variant_place 增流) referenced_by: [棋盘生成, 战斗结算, 地图战斗资源, 冒险结构, 存档序列化] - name: difficulty_row_resolution @@ -89,25 +90,26 @@ entities: type: schema_data owner: design/gdd/systems/排雷循环/地形变异.md attributes: - table: VariationTable(数据配置 承载,键 = variant_id;地形变异 回填 owner,2026-09-08 定稿回填) - fields: variant_id(String 主键唯一)/ display_name / unlock_level(int [1,999],knob)/ scope(enum region|global,global 预留 难度曲线与Boss)/ info_mask(enum none|hide_numbers)/ cell_constraint(enum any|safe_nonzero)/ biome_tag(StringList,∅=通配,knob)/ counter_item_ids(StringList,魔法道具 预留,非 knob——2026-09-08 re-review 修正 ✓ 误标) - mvp_content: 空清单(合法态)——无任何机制已定稿的变异类型;岩浆/冰冻只登记名字、无 MVP 行(垂直切片随关卡解锁建行,建行批随批配 ≥1 反制道具——aux_items 框架内容契约) - zero_special_casing: 连锁/数字计数/清图/旗/价目/结算零特判——既有系统行为不感知变异状态(排雷玩法 执行逻辑无变异分支;地形变异 Rule 3 的推论) + table: VariationTable(数据配置 承载,键 = variant_id;地形变异 回填 owner,2026-09-08 定稿回填;2026-09-20 首批类型定稿批字段集枚举化重写) + fields: variant_id(String 主键唯一)/ display_name / unlock_level(int [1,999],knob)/ scope(enum region|global,global 预留 难度曲线与Boss)/ effect(enum none|flat_damage|hide_number,词表可扩充)/ effect_value(int [1,9],仅 flat_damage 消费,加法核小整数 knob)/ trigger(enum on_open|on_number_shown,词表可扩充)/ placement(enum closed_only|any,词表可扩充)/ board_types(StringList,∅=通配,knob——2026-09-20 用户裁定由 biome_tag 更名对齐 board_type 词表)/ counter_item_ids(StringList,魔法道具 预留,非 knob——首批四行恒空,Alpha 反制批回填)/ 表头布置参数 percent_of_cells=5 · per_level_bonus=2 · pack_max=4(knob,难度曲线与Boss) + mvp_content: 四行两轴×两关(2026-09-20 首批类型定稿批)——thorn 荆棘丛(flat_damage/on_open/closed_only/dungeon/1)· vine 藤蔓缠绕(hide_number/on_number_shown/any/dungeon)· magma 岩浆(flat_damage/on_open/closed_only/underdark/2,登记名沿用)· ash 烬覆(hide_number/on_number_shown/any/underdark);行值 provisional 归 难度曲线与Boss;冰冻占位名退役 + zero_special_casing: 连锁/数字计数/清图/旗/价目/结算零特判——既有系统行为不感知变异状态(排雷玩法 执行逻辑无变异分支;地形变异 Rule 3 的推论);读取方按 effect/trigger/placement 枚举路由(战斗结算 兑付段 / 呈现层 遮蔽回调),不按类型名分支 rule4_exemption: 定义表非进度成长表,键 = variant_id,进度语义承载于 unlock_level 列(数据配置 Rule 4 豁免,schema 注释承载) - cross_field_validation: info_mask=hide_numbers ⇒ cell_constraint=safe_nonzero——VariationTable schema validate() 跨字段类别,违规加载期致命(2026-09-08 re-review 增;防未来 hide_numbers+any 行静默击穿 EC4 连锁停止;地形变异 夹具 variation_mask_constraint 承载) - dangling_ref_policy: counter_item_ids 为 数据配置 的引用型字段——现行空清单下无引用行,取值悬空;首个真实引用随未来变异行建行批回填,届时激活悬空校验(数据配置 修订批;条件性夹具 aux_dangling_counter 登记 魔法道具 GDD AC-13) - referenced_by: [棋盘生成, 排雷玩法, 魔法道具] + cross_field_validation: flat_damage ⇒ trigger=on_open ∧ placement=closed_only ∧ effect_value∈[1,9];hide_number ⇒ trigger=on_number_shown——VariationTable schema validate() 跨字段类别,违规加载期致命(2026-09-20 首批类型定稿批重立;旧 hide_numbers⇒safe_nonzero 闸随字段集退役) + placement_segment: 变异格赋予 = 棋盘设置顺序第 4 步(2026-09-19 用户拍板:生成棋盘 → 打开状态初始化 → 地图战斗资源格设置 → 变异格设置)——执行面 = 战斗结算 编排段 _place_variants,专用随机流 variant_place(一板恰一 scope);生成器管线恒零布置 + dangling_ref_policy: counter_item_ids 为 数据配置 的引用型字段——首批四行恒空无引用行,取值悬空;Alpha 反制批回填时激活悬空校验(数据配置 修订批;条件性夹具 aux_dangling_counter 登记 魔法道具 GDD AC-13) + referenced_by: [棋盘生成, 排雷玩法, 魔法道具, 战斗结算] - name: variant_pool_gate type: formula owner: design/gdd/systems/排雷循环/地形变异.md attributes: - variables: [unlock_level, level_index, biome_tag, board_type] - rule: pool = {v ∈ VariationTable | unlock_level ≤ level_index ∧ (biome_tag=∅ ∨ biome_tag ∋ board_type)};直接整数比较(无 数据配置 R1 floor);∅ = 通配语义 - output_range: pool 集合(现行空集——无内容行;空池合法——棋盘生成 静默布置普通图,不报错) + variables: [unlock_level, level_index, board_types, board_type] + rule: pool = {v ∈ VariationTable | unlock_level ≤ level_index ∧ (board_types=∅ ∨ board_types ∋ board_type)};直接整数比较(无 数据配置 R1 floor);∅ = 通配语义 + output_range: pool 集合(现行 L1 dungeon = {thorn, vine} / L2 underdark = {magma, ash};空池合法——静默布置普通图,不报错) board_type: String(值域 = 棋盘生成 BoardGenTable 词表 v1.0 {dungeon, underdark},2026-09-19 定稿;域校验 = 词表成员检查,fail-fast 闭合) - downstream: 棋盘生成 布置候选池门控(棋盘生成 依赖与输出节);多变异选取须消费 board_gen 流(回放确定性) - referenced_by: [棋盘生成] + downstream: 战斗结算 布置段候选池门控(2026-09-20 起布置执行位 = 战斗初始化第 4 步,非棋盘生成管线);多变异选取须消费 variant_place 流(回放确定性) + referenced_by: [战斗结算, 棋盘生成] - name: status_effect type: schema_data diff --git a/prototypes/dungeon-sweeper-vertical-slice/core/battle_orchestrator.gd b/prototypes/dungeon-sweeper-vertical-slice/core/battle_orchestrator.gd index 60909d30..1d862054 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/core/battle_orchestrator.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/core/battle_orchestrator.gd @@ -150,6 +150,9 @@ func _install_new_board(during_round := false) -> bool: if not _reward_economy.roll_board(eligible): _fatal("地图战斗资源 roll_board 被拒") return false + # 地形变异 布置段(2026-09-19 用户拍板棋盘设置顺序第 4 步:生成棋盘 → 打开状态初始化 → + # 战斗资源格设置 → 变异格设置)——布置在初始展开与资源格之后,变异天然不落已开区 + _place_variants(board) _flag_consumed.clear() # 换图新雷 = 新实例,资格自然重置(行动经济 Rule 8) _flag_miss_seen.clear() _perfect_hit_true = 0 # 板级完美计数随 install 重置 @@ -160,6 +163,58 @@ func _install_new_board(during_round := false) -> bool: return true +# ---- 地形变异 布置段(首批类型定稿批 2026-09-19——棋盘设置顺序第 4 步)---- +# 候选池门控(unlock_level ≤ 关卡序号 + board_types 与本地形交集,∅ 通配)经 config_db.variant_pool; +# 池空 = 合法态,静默零布置(框架 P0 口径不变)。零特判:本段只写格子变异数据快照,不改扫雷规则本体。 +# 候选格域 = 安全格 − 初始展开区(伤害型 placement=closed_only 的结构保障——布置发生在初始展开之后, +# 已开格天然排除);雷格不吃变异(挖雷一层惩罚 + 变异伤害双层叠罚污染逐行归因,首版排除——provisional)。 +# 每图至多一种类型(候选池内均匀选一,混型布置归因复杂留待后续批);数量 = round(总格数×percent) + +# (关卡序号−1)×per_level_bonus;连通片生长(8 邻),单片 ≤ pack_max(2×2 / 1×4 / 1×3+1 等形)。 +func _place_variants(board: BoardData) -> void: + var pool: Array = db.variant_pool(int(encounter.level_index), String(board.board_type)) + if pool.is_empty(): + return + var r0 := {} + for c in board.compute_r0(): + r0[c] = true + var candidates: Array = [] + for c in board.safe_cells(): + if not r0.has(c): + candidates.append(c) + if candidates.is_empty(): + return + var stream: RandomNumberGenerator = rng.open_stream("variant_place") # 一板恰一 scope(换图/刷新图各取号一次) + var vrow: Dictionary = pool[stream.randi_range(0, pool.size() - 1)] + var total: int = int(round(float(board.width * board.height) * float(db.variation.percent_of_cells) / 100.0)) \ + + (int(encounter.level_index) - 1) * int(db.variation.per_level_bonus) + total = mini(total, candidates.size()) + var placed := {} + while placed.size() < total: + var pack_size: int = mini(total - placed.size(), stream.randi_range(2, int(db.variation.pack_max))) + var free: Array = [] + for c in candidates: + if not placed.has(c): + free.append(c) + if free.is_empty(): + break + var seed_c: Vector2i = free[stream.randi_range(0, free.size() - 1)] + placed[seed_c] = true + var pack: Array = [seed_c] + while pack.size() < pack_size: + var fringe: Array = [] + for c in pack: + for n in board.neighbors8(c): + if candidates.has(n) and not placed.has(n) and not fringe.has(n): + fringe.append(n) + if fringe.is_empty(): + break # 生长域耗尽——该片就地收束(数量不足总目标为合法态,候选域碎片化时触发) + var nxt: Vector2i = fringe[stream.randi_range(0, fringe.size() - 1)] + placed[nxt] = true + pack.append(nxt) + for c in placed.keys(): + board.variants[c] = [String(vrow.variant_id)] + + # ---- 事件缓冲(轮 facts 收集;结算触发 = Waiting 相位编排点,不依赖到达序)---- func _on_cell_revealed(evt: Dictionary) -> void: @@ -219,6 +274,18 @@ func _settle() -> void: # 插旗不可逆四情况②:插雷格再挖开 = 只受挖雷一层惩罚(mine_hit 照常兑付——惊动巢穴), # 标中不发(旗已随挖掘擦除、不入轮末判定基线)。 _grant_routed(snap, "negative", "mine_hit", evt.cell, ri, "") + # 地形变异 兑付段(授予段尾——触发枚举 on_open:挖开即伤、chain 通道同口径,轮末结算逐行兑现; + # 信息型 hide_number 不进结算——呈现层读取,见 board_view 数字遮蔽;雷格恒无变异——布置段排除, + # 双层叠罚不存在;插旗格直挖照吃变异伤害——地形后果同 mine_hit 口径,非奖励不作废) + for evt in _facts_reveals: + var vids: Array = evt.get("variant_ids", []) + for vid in vids: + var vrow: Dictionary = db.variation.row_by_id(String(vid)) + if String(vrow.get("effect", "")) != "flat_damage": + continue # 触发词表按 effect 路由:flat_damage=on_open 兑现;hide_number/on_open 外组合 = 数据行新词表未接线(显式忽略面) + var dmg: int = int(vrow.get("effect_value", 0)) + _enemy_system.apply_damage(_enemy_system.hero_unit(), dmg) + _emit_row("damage", "地形变异", evt.cell, ri, "%s → 主角" % String(vrow.get("display_name", vid)), dmg, {"target": "hero", "variant_id": String(vid)}, "") for evt in _facts_flags: _judge_flag(evt, ri) if _facts_path == "cleared": diff --git a/prototypes/dungeon-sweeper-vertical-slice/core/config_db.gd b/prototypes/dungeon-sweeper-vertical-slice/core/config_db.gd index 9bff8d2f..fad7e41c 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/core/config_db.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/core/config_db.gd @@ -301,14 +301,15 @@ func _ceil_key(sorted_keys: Array, request: int) -> int: return -1 -# ---- 地形变异 variant_pool_gate:候选池门控(∅ biome_tag = 通配)---- +# ---- 地形变异 variant_pool_gate:候选池门控(∅ board_types = 通配;2026-09-19 用户裁定字段名对齐 +# board_type 词表——biome_tags 系旧观念残留退役)---- func variant_pool(level_index: int, board_type: String) -> Array: var pool: Array = [] for row in variation.rows: if int(row.get("unlock_level", 999)) > level_index: continue - var tags: Array = row.get("biome_tags", []) + var tags: Array = row.get("board_types", []) if tags.is_empty(): pool.append(row) continue diff --git a/prototypes/dungeon-sweeper-vertical-slice/core/rng_service.gd b/prototypes/dungeon-sweeper-vertical-slice/core/rng_service.gd index a5f3a702..7671177f 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/core/rng_service.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/core/rng_service.gd @@ -7,7 +7,7 @@ # 取号语义:全新实例首个 open_stream = scope 0(地图战斗资源 预推进术语的登记口径)。 extends Node -const STREAM_NAMES := ["board_gen", "reward_roll", "encounter", "armor_roll", "behavior_roll"] # 2026-09-12 增 encounter(遭遇随机化批);2026-09-16 增 armor_roll(护甲等级掷点批)+ behavior_roll(敌人技能池批——开轮摇号,每敌派生独立子流,registry rng_service 条目同步) +const STREAM_NAMES := ["board_gen", "reward_roll", "encounter", "armor_roll", "behavior_roll", "variant_place"] # 2026-09-12 增 encounter(遭遇随机化批);2026-09-16 增 armor_roll(护甲等级掷点批)+ behavior_roll(敌人技能池批——开轮摇号,每敌派生独立子流,registry rng_service 条目同步);2026-09-19 增 variant_place(地形变异首批类型定稿批——变异布置流,一板恰一 scope) # 分段表达式书写(数据配置 R2 实现级规约——禁 ≥2^63 完整字面量) const GOLDEN_RATIO := (0x9E3779B9 << 32) | 0x7F4A7C15 # 0x9E3779B97F4A7C15 @@ -16,6 +16,7 @@ const SALT_REWARD_ROLL := (0xE7037ED1 << 32) | 0xA0B428DB # 0xE7037ED1A0B428DB const SALT_ENCOUNTER := (0x8EBC6AF0 << 32) | 0x9C88C6E3 # 0x8EBC6AF09C88C6E3(遭遇骰——registry stream_seed_derivation 权威值;旧值 0x5DEECE66D<<32 超 int64 被截断,黄金向量漂移) const SALT_ARMOR_ROLL := (0xC2B2AE3D << 32) | 0x27D4EB2F # 0xC2B2AE3D27D4EB2F(护甲掷点流——互异奇常数 ≠ 乘数;黄金向量见冒烟/registry 同批登记) const SALT_BEHAVIOR_ROLL := (0x85EBCA77 << 32) | 0x58C24E15 # 0x85EBCA7758C24E15(技能摇号流——互异奇常数 ≠ 乘数;黄金向量见冒烟/registry 同批登记) +const SALT_VARIANT_PLACE := (0x9AE16A3F << 32) | 0x2FAD6C39 # 0x9AE16A3F2FAD6C39(变异布置流——互异奇常数 ≠ 乘数;地形变异首批类型定稿批 2026-09-19) # behavior_roll 派生向量(registry 同批登记):子流 scope = spawn_index × STRIDE + 单敌摇号序(0 起)。 # 每敌消费自己的 scope 段 → 单敌出招轨迹可独立复放、敌间互不穿插(proposal 约束二)。 const BEHAVIOR_SUBSTREAM_STRIDE := 1000000 @@ -27,7 +28,7 @@ const INT64_MIN := -9223372036854775808 var _master_seed: int = -1 # -1 = 未播种(取数即致命,EC7) var master_seed: int: get: return _master_seed -var _counters := {"board_gen": 0, "reward_roll": 0, "encounter": 0, "armor_roll": 0, "behavior_roll": 0} +var _counters := {"board_gen": 0, "reward_roll": 0, "encounter": 0, "armor_roll": 0, "behavior_roll": 0, "variant_place": 0} func _salt_for(stream: String) -> int: @@ -42,6 +43,8 @@ func _salt_for(stream: String) -> int: return SALT_ARMOR_ROLL "behavior_roll": return SALT_BEHAVIOR_ROLL + "variant_place": + return SALT_VARIANT_PLACE push_error("[RngService] 未知名流: %s" % stream) assert(false, "未知名流(流集合封闭,EC12)") return 0 @@ -63,10 +66,11 @@ func set_master_seed(seed_val: int) -> void: if seed_val < 0: seed_val = -seed_val push_warning("[RngService] master_seed 负输入 abs 归一化: %d" % seed_val) - assert(_counters["board_gen"] == 0 and _counters["reward_roll"] == 0 and _counters["encounter"] == 0 and _counters["armor_roll"] == 0 and _counters["behavior_roll"] == 0, "冒险中途重播种会击穿同冒险无碰撞前提(仅许全部流计数器全零时调用)") + assert(_counters["board_gen"] == 0 and _counters["reward_roll"] == 0 and _counters["encounter"] == 0 and _counters["armor_roll"] == 0 and _counters["behavior_roll"] == 0 and _counters["variant_place"] == 0, "冒险中途重播种会击穿同冒险无碰撞前提(仅许全部流计数器全零时调用)") _master_seed = seed_val _counters["board_gen"] = 0 _counters["reward_roll"] = 0 + _counters["variant_place"] = 0 # 切片登记:冒险开始重播种入口(正式侧调用权归 冒险结构;切片主菜单 = 冒险起点) @@ -76,6 +80,7 @@ func new_adventure(seed_val: int) -> void: _counters["encounter"] = 0 _counters["armor_roll"] = 0 _counters["behavior_roll"] = 0 + _counters["variant_place"] = 0 set_master_seed(seed_val) diff --git a/prototypes/dungeon-sweeper-vertical-slice/data/schemas/variation_table.gd b/prototypes/dungeon-sweeper-vertical-slice/data/schemas/variation_table.gd index 8d2550e5..40cd0ef1 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/data/schemas/variation_table.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/data/schemas/variation_table.gd @@ -1,16 +1,27 @@ # VERTICAL SLICE - NOT FOR PRODUCTION # Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2) # Date: 2026-09-10 -# 数据配置 VariationTable schema(地形变异 回填值;行 = Dictionary,键 variant_id)。 -# 依据 design/archive/gdd-2026-09-14/a4-terrain-variation.md "VariationTable Schema 定稿"。 +# 数据配置 VariationTable schema(地形变异 首批类型定稿批 2026-09-19;行 = Dictionary,键 variant_id)。 +# 字段集(2026-09-19 用户裁定:枚举抽象,不写死类型):effect/effect_value/trigger/placement 四字段 +# 把"变异是什么、在哪出现、何时起作用"全部数据化——新增类型 = 清单加行 + 必要时扩充枚举词表,不改读取方。 +# biome_tags 更名 board_types(用户裁定:门控匹配对象即 board_type 词表,biome 系旧观念残留不沿用); +# counter_items 字段位保留(反制道具绑定,归 Alpha 反制批回填,切片期恒空)。 +# info_mask/cell_constraint 两旧字段随枚举化退役(hide_number 并入 effect 词表,safe_nonzero 由 +# 布置域结构保障——初始展开区天然排除,见 battle_orchestrator._place_variants)。 class_name VariationTable extends Resource @export var rows: Array = [] # Array[Dictionary] +# 布置参数(表头字段;数量公式 = round(总格数 × percent_of_cells / 100) + (关卡序号 − 1) × per_level_bonus, +# 连通片生长、单片 ≤ pack_max——2026-09-19 用户拍板:图规模百分比 × 关卡递增,全连通、单片不超 4) +@export var percent_of_cells: int = 5 +@export var per_level_bonus: int = 2 +@export var pack_max: int = 4 const SCOPES := ["region", "global"] -const INFO_MASKS := ["none", "hide_numbers"] -const CELL_CONSTRAINTS := ["any", "safe_nonzero"] +const EFFECTS := ["none", "flat_damage", "hide_number"] # 效果词表:直伤 / 数字遮蔽(可扩充) +const TRIGGERS := ["on_open", "on_number_shown"] # 触发词表:被打开即触发 / 打开且显示数字时触发(可扩充) +const PLACEMENTS := ["closed_only", "any"] # 布置约束词表:只落布置时未开格 / 任意格(可扩充) func validate() -> Array: @@ -32,13 +43,27 @@ func validate() -> Array: errs.append({"category": "domain", "message": "%s unlock_level 越域 [1,999]: %d" % [vid, ul]}) if not SCOPES.has(row.get("scope", "")): errs.append({"category": "enum", "message": "%s scope 出域: %s" % [vid, row.get("scope")]}) - if not INFO_MASKS.has(row.get("info_mask", "")): - errs.append({"category": "enum", "message": "%s info_mask 出域: %s" % [vid, row.get("info_mask")]}) - if not CELL_CONSTRAINTS.has(row.get("cell_constraint", "")): - errs.append({"category": "enum", "message": "%s cell_constraint 出域: %s" % [vid, row.get("cell_constraint")]}) - # 跨字段(地形变异 re-review 增补,加载期致命):hide_numbers ⇒ safe_nonzero - if row.get("info_mask", "") == "hide_numbers" and row.get("cell_constraint", "") != "safe_nonzero": - errs.append({"category": "cross_field", "message": "%s hide_numbers ⇒ cell_constraint 必为 safe_nonzero" % vid}) + if not EFFECTS.has(row.get("effect", "")): + errs.append({"category": "enum", "message": "%s effect 出域: %s" % [vid, row.get("effect")]}) + if not TRIGGERS.has(row.get("trigger", "")): + errs.append({"category": "enum", "message": "%s trigger 出域: %s" % [vid, row.get("trigger")]}) + if not PLACEMENTS.has(row.get("placement", "")): + errs.append({"category": "enum", "message": "%s placement 出域: %s" % [vid, row.get("placement")]}) + # 跨字段互锁(加载期致命——枚举语义一致性随首批类型定稿批 2026-09-19 立闸): + # 伤害型 = 挖开即伤 ⇒ 触发 on_open + 只落未开格(closed_only)+ 伤害量为加法核小整数 + if row.get("effect", "") == "flat_damage": + if row.get("trigger", "") != "on_open": + errs.append({"category": "cross_field", "message": "%s flat_damage ⇒ trigger 必为 on_open" % vid}) + if row.get("placement", "") != "closed_only": + errs.append({"category": "cross_field", "message": "%s flat_damage ⇒ placement 必为 closed_only(只出现在未打开的格子)" % vid}) + var ev: int = int(row.get("effect_value", 0)) + if ev < 1 or ev > 9: + errs.append({"category": "domain", "message": "%s effect_value 越域 [1,9](加法核小整数): %d" % [vid, ev]}) + # 信息型 = 被打开且显示数字时起作用 ⇒ 触发 on_number_shown + if row.get("effect", "") == "hide_number" and row.get("trigger", "") != "on_number_shown": + errs.append({"category": "cross_field", "message": "%s hide_number ⇒ trigger 必为 on_number_shown" % vid}) + # 信息遮蔽型只允许落安全格(遮蔽对象是数字;雷格无数字可遮)——placement=any 时由读取方 + # 呈现面按 kind=safe 自然短路,此处不设布置约束 return errs diff --git a/prototypes/dungeon-sweeper-vertical-slice/data/tables/variation.tres b/prototypes/dungeon-sweeper-vertical-slice/data/tables/variation.tres index ec475565..43c2df05 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/data/tables/variation.tres +++ b/prototypes/dungeon-sweeper-vertical-slice/data/tables/variation.tres @@ -4,8 +4,60 @@ [resource] script = ExtResource("1") -rows = [] +percent_of_cells = 5 +per_level_bonus = 2 +pack_max = 4 +rows = [{ +"variant_id": "thorn", +"display_name": "荆棘丛", +"unlock_level": 1, +"scope": "region", +"effect": "flat_damage", +"effect_value": 1, +"trigger": "on_open", +"placement": "closed_only", +"board_types": ["dungeon"], +"counter_items": [] +}, { +"variant_id": "vine", +"display_name": "藤蔓缠绕", +"unlock_level": 1, +"scope": "region", +"effect": "hide_number", +"effect_value": 0, +"trigger": "on_number_shown", +"placement": "any", +"board_types": ["dungeon"], +"counter_items": [] +}, { +"variant_id": "magma", +"display_name": "岩浆", +"unlock_level": 2, +"scope": "region", +"effect": "flat_damage", +"effect_value": 2, +"trigger": "on_open", +"placement": "closed_only", +"board_types": ["underdark"], +"counter_items": [] +}, { +"variant_id": "ash", +"display_name": "烬覆", +"unlock_level": 2, +"scope": "region", +"effect": "hide_number", +"effect_value": 0, +"trigger": "on_number_shown", +"placement": "any", +"board_types": ["underdark"], +"counter_items": [] +}] -; 地形变异 清单为空是合法态(地形变异 P0):零变体行 → 棋盘生成零布置,行为与不存在本系统时一致。 -; 首批变异类型(岩浆/冰冻,机制未定)定稿时在本清单加一行声明即应生效,无需改代码。雾格已整体 -; 移除(见 design/gdd/05-不做清单.md)——本表不再有 fog 行。 +; 地形变异 首批类型定稿批(2026-09-19 用户拍板)——四行两轴×两关: +; 第一关 dungeon(平原/密林基线):荆棘丛(伤害型:挖开扣 1 点生命,轮末结算兑现)+ 藤蔓缠绕 +; (信息型:挖开且显示数字时数字被遮蔽,呈现层读取);第二关 underdark(幽暗地域):岩浆 +; (伤害型 2 点,登记名沿用)+ 烬覆(信息型)。unlock_level 门控 + board_types 交集门控双闸, +; 首关候选池恒 {荆棘丛, 藤蔓缠绕},第二关候选池恒 {岩浆, 烬覆}。 +; counter_items 恒空:反制道具配套义务随框架契约保留,兑现归 Alpha 反制批(2026-09-19 用户拍板 +; 反制道具改排 Alpha——切片期变异暂无反制手段,纯税状态已知悉接受)。 +; 全部行值 provisional 归 难度曲线与Boss 调参批(effect_value/布置参数均为加法核小整数轴)。 diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/main.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/main.gd index e1c8ac4a..318b0af2 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/main.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/presentation/main.gd @@ -99,6 +99,8 @@ func _build_ui() -> void: # 奖励徽章数据源:地图战斗资源 身份解析只读回调(已开格身份 icon——挖掘期 Active 相位内有效) board_view.set_identity_resolver(_identity_lookup) + # 数字遮蔽数据源:地形变异 hide_number 解析只读回调(信息型呈现——board_view 渲染面读取) + board_view.set_variant_mask_resolver(_variant_mask_lookup) logger = PlaytestLogger.new() logger.ap_per_round = int(db.action_economy.ap_per_round) @@ -329,6 +331,20 @@ func _identity_lookup(c: Vector2i): return settlement.query_reward_identity(c) +# 地形变异 数字遮蔽解析(信息型 hide_number——格带该效果变异即遮蔽;读取方按格子变异数据行动) +func _variant_mask_lookup(c: Vector2i) -> bool: + if settlement == null: + return false + var b = settlement.minesweeper.current_board() + if b == null: + return false + for vid in b.variant_ids(c): + var vrow: Dictionary = settlement.db.variation.row_by_id(String(vid)) + if String(vrow.get("effect", "")) == "hide_number": + return true + return false + + func _show_board_fx_for_node(node_type: String = "") -> void: # Boss 遭遇变体:紫赤影子爬棋盘一次性占位演出(§2.6) if node_type == "boss": diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/board_view.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/board_view.gd index 2338a4db..3b8f387d 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/board_view.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/board_view.gd @@ -40,6 +40,7 @@ var _marker := {} # Vector2i → {"dot": ColorRect, "badge": Label, "h var _board: BoardData = null var _minesweeper = null # 排雷玩法 只读查询引用(refresh 更新;null = 无战斗) var _identity_cb := Callable() # 地图战斗资源 身份解析回调(组合根注入只读面)——Vector2i → Dictionary/null +var _mask_cb := Callable() # 地形变异 数字遮蔽回调(组合根注入只读面)——Vector2i → bool(true = 该格数字不显示) var _diggable := {} # 可挖域(8 向相邻未开格)——呼吸微光作用集 var _breath_t := 0.0 var _dig_glow := false # 挖掘期才允许唯一在动元素(art-bible §2.1) @@ -92,6 +93,12 @@ func set_identity_resolver(cb: Callable) -> void: _identity_cb = cb +# 组合根注入 地形变异 数字遮蔽只读回调(信息型 hide_number 触发面——触发枚举 on_number_shown: +# 已开且显示数字时起作用;读取方按格子变异数据行动,规则本体零特判) +func set_variant_mask_resolver(cb: Callable) -> void: + _mask_cb = cb + + # D 区可用面积(AP 条之外)→ 格边长 = floor((min(宽, 高) − 预留 − (N−1)×格间距) / N)(battle.md 格尺寸公式) # 2026-09-19 修复:格边长公式补算 (N−1)×2px 网格间距 + 垂直预留 24px——旧公式只算 N×cell, # L2 12×12 板网格实高溢出可用区 ~6px,下缘被 clip_contents 裁掉约 1/6 格高。 @@ -313,7 +320,10 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void: bg = COL_FLOOR var d: int = rec.get("digit", 0) if d > 0: - text = str(d) + if _mask_cb.is_valid() and bool(_mask_cb.call(c)): + text = "?" # 数字遮蔽(地形变异 hide_number——信息型:该格真实数字不显示) + else: + text = str(d) else: if flag: bg = COL_BRICK diff --git a/prototypes/dungeon-sweeper-vertical-slice/smoke/rhythm_batch.gd.uid b/prototypes/dungeon-sweeper-vertical-slice/smoke/rhythm_batch.gd.uid new file mode 100644 index 00000000..320afea6 --- /dev/null +++ b/prototypes/dungeon-sweeper-vertical-slice/smoke/rhythm_batch.gd.uid @@ -0,0 +1 @@ +uid://cc2yhv5d20gfk diff --git a/prototypes/dungeon-sweeper-vertical-slice/smoke/smoke_all.gd b/prototypes/dungeon-sweeper-vertical-slice/smoke/smoke_all.gd index b921502e..4d33046c 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/smoke/smoke_all.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/smoke/smoke_all.gd @@ -68,6 +68,8 @@ func _run_core_smoke(db, rng) -> void: _smoke_board_generator() _smoke_layout_cell_formula() _smoke_variation_framework() + _smoke_variant_placement(1) + _smoke_variant_placement(2) _smoke_full_battle(1, 0, "L1 · 地精") _smoke_full_battle(2, 0, "L2 · 兄弟精") _smoke_level() @@ -484,7 +486,7 @@ func _smoke_board_generator() -> void: # 但必须打印实测值(正常出口);accept_worst 由下方 illegal_product 断言承担。 check(inf >= float(row.inferability_target) or mode == "accept_best", "L%d inferability %.3f %s target %.2f(accept_mode=%s)" % [L, inf, "≥" if inf >= float(row.inferability_target) else "<", float(row.inferability_target), mode]) - check(b1.variants.is_empty(), "L%d 零变体格(地形变异 清单为空 → 生成期零布置)" % L) + check(b1.variants.is_empty(), "L%d 生成器产物零变体格(变异布置归战斗初始化第 4 步——生成器管线不布置,2026-09-19 用户拍板棋盘设置顺序)" % L) check(b1.metrics.has("degraded_product") and b1.metrics.has("target_mines"), "L%d 指标集输出降级标记与目标雷数(三分出口表)" % L) check(b1.degraded_product == (mode == "accept_best"), "L%d 降级产物标记 = (accept_mode==accept_best)(%s→%s)" % [L, mode, str(b1.degraded_product)]) check(b1.illegal_product == (mode == "accept_worst"), "L%d 非法产物标记 = (accept_mode==accept_worst)(%s→%s)" % [L, mode, str(b1.illegal_product)]) @@ -506,13 +508,131 @@ func _mk_stream(stream_name: String, scope: int) -> RandomNumberGenerator: return rg -# ---- 地形变异 框架面(P0 空清单合法 + 候选池门控通道在位——框架惰性非废弃)---- +# ---- 地形变异 框架面(首批类型定稿批 2026-09-19:四行两轴×两关 + 门控 + 跨字段互锁)---- func _smoke_variation_framework() -> void: - print("--- 地形变异 框架面(空清单合法 + 候选池门控通道)---") - check(_db.variation.rows.is_empty(), "变异清单为空(P0 合法态——零变体格)") - check(_db.variant_pool(1, "dungeon").is_empty(), "候选池为空——棋盘生成静默产出无变异图(不报错、不警告)") + print("--- 地形变异 框架面(首批四类型 + 候选池门控 + 跨字段互锁)---") + check(_db.variation.rows.size() == 4, "变异清单 4 行(荆棘丛/藤蔓缠绕/岩浆/烬覆——首批类型定稿批)") + check(_db.variation.validate().is_empty(), "变异表校验零错误(4 行全过域与跨字段互锁)") + var pool1_ids: Array = [] + for r in _db.variant_pool(1, "dungeon"): + pool1_ids.append(String(r.variant_id)) + check(pool1_ids == ["thorn", "vine"], "L1 dungeon 候选池 = 荆棘丛+藤蔓缠绕(unlock + board_types 双闸)") + var pool2_ids: Array = [] + for r in _db.variant_pool(2, "underdark"): + pool2_ids.append(String(r.variant_id)) + check(pool2_ids == ["magma", "ash"], "L2 underdark 候选池 = 岩浆+烬覆(第二关解锁)") + check(_db.variant_pool(1, "underdark").is_empty(), "unlock 门控:L1 不进 underdark 行(岩浆/烬覆第二关解锁)") + var pool2d_ids: Array = [] + for r in _db.variant_pool(2, "dungeon"): + pool2d_ids.append(String(r.variant_id)) + check(pool2d_ids == ["thorn", "vine"], "board_types 门控:L2 dungeon 板池 = 荆棘丛+藤蔓缠绕(解锁累积;岩浆/烬覆被 board_types ∅ 交集排除)") check(_db.variation.row_by_id("nonexistent").is_empty(), "清单外 variant_id 返回空(读取通道在位)") + # 跨字段互锁负样本(加载期致命——枚举语义一致性闸) + var vt = load("res://data/schemas/variation_table.gd").new() + vt.rows = [{"variant_id": "bad1", "display_name": "坏行甲", "unlock_level": 1, "scope": "region", + "effect": "flat_damage", "effect_value": 1, "trigger": "on_open", "placement": "any", + "board_types": [], "counter_items": []}] + check(_errs_has_cross_field(vt.validate()), "跨字段互锁负样本①:flat_damage + placement=any 被拒(只出现在未打开的格子)") + vt.rows = [{"variant_id": "bad2", "display_name": "坏行乙", "unlock_level": 1, "scope": "region", + "effect": "hide_number", "effect_value": 0, "trigger": "on_open", "placement": "any", + "board_types": [], "counter_items": []}] + check(_errs_has_cross_field(vt.validate()), "跨字段互锁负样本②:hide_number + trigger≠on_number_shown 被拒") + + +func _errs_has_cross_field(errs: Array) -> bool: + for e in errs: + if String(e.get("category", "")) == "cross_field": + return true + return false + + +# ---- 地形变异 布置段(首批类型定稿批 2026-09-19——棋盘设置顺序第 4 步断言面)---- +# 布置发生在编排器战斗初始化(install 初始展开 + roll_board 资源格之后),生成器产物零变异—— +# 本段经 begin_battle 走完整初始化序后断言:门控一致性 / 布置域 / 数量公式 / 连通片上限 / 确定性。 + +func _smoke_variant_placement(L: int) -> void: + print("--- 地形变异 布置段(L%d)---" % L) + var rng = load("res://core/rng_service.gd").new() + rng.set_master_seed(42) + var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng) + if not settlement.begin_battle({"level_index": L, "node_depth": 0, "hero_hp": _db.hero.max_hp}): + check(false, "L%d 布置段战斗启动失败" % L) + return + var b = settlement.minesweeper.current_board() + check(b != null, "L%d 布置段板非空" % L) + if b == null: + return + var pool_ids: Array = [] + for r in _db.variant_pool(L, String(b.board_type)): + pool_ids.append(String(r.variant_id)) + var vcells: Array = b.variants.keys() + var vid0 := "" + var single_type := true + var bad_id := false + for c in vcells: + var ids: Array = b.variants[c] + if ids.size() != 1: + single_type = false + for vid in ids: + if vid0 == "": + vid0 = String(vid) + elif vid0 != String(vid): + single_type = false + if not pool_ids.has(String(vid)): + bad_id = true + check(not bad_id, "L%d 变异 id 全在候选池(门控一致性:%s)" % [L, str(pool_ids)]) + check(single_type, "L%d 每图至多一种变异类型(单格单 id + 全图同 id)" % L) + var r0 := {} + for c in b.compute_r0(): + r0[c] = true + var domain_ok := true + for c in vcells: + if r0.has(c) or b.is_mine(c): + domain_ok = false + check(domain_ok, "L%d 变异格全在初始展开区外且不吃雷格(布置域结构保障——closed_only)" % L) + var expect_cap: int = int(round(float(b.width * b.height) * float(_db.variation.percent_of_cells) / 100.0)) \ + + (L - 1) * int(_db.variation.per_level_bonus) + check(vcells.size() <= expect_cap, "L%d 变异格数 %d ≤ 公式上限 %d(图规模百分比 + 关卡递增)" % [L, vcells.size(), expect_cap]) + check(vcells.size() > 0, "L%d 种子 42 板非零布置(候选池非空下应有遭遇面)" % L) + var pack_max: int = int(_db.variation.pack_max) + var vset := {} + for c in vcells: + vset[c] = true + var seen := {} + var pack_ok := true + for c in vcells: + if seen.has(c): + continue + seen[c] = true + var comp_size := 1 + var q: Array = [c] + while q.size() > 0: + var x: Vector2i = q.pop_front() + for n in b.neighbors8(x): + if vset.has(n) and not seen.has(n): + seen[n] = true + comp_size += 1 + q.append(n) + if comp_size > pack_max: + pack_ok = false + check(pack_ok, "L%d 全部连通片 ≤ %d 格(全连通生长、2×2/1×4/1×3+1 等形)" % [L, pack_max]) + # 布置确定性:全新 RngService 同种子重开,变异布置逐格相同(variant_place 流确定性) + var rng2 = load("res://core/rng_service.gd").new() + rng2.set_master_seed(42) + var s2 = load("res://core/battle_orchestrator.gd").new(_db, rng2) + s2.begin_battle({"level_index": L, "node_depth": 0, "hero_hp": _db.hero.max_hp}) + var b2 = s2.minesweeper.current_board() + check(b2 != null and _variant_dump(b2) == _variant_dump(b), "L%d 同种子变异布置逐格相同(variant_place 流确定性)" % L) + + +func _variant_dump(b) -> String: + var keys: Array = b.variants.keys() + keys.sort_custom(func(a, b2): return a.y * 1000 + a.x < b2.y * 1000 + b2.x) + var parts: Array = [] + for c in keys: + parts.append("%d,%d:%s" % [c.x, c.y, str(b.variants[c])]) + return "|".join(parts) func _board_dump(b) -> String: diff --git a/prototypes/dungeon-sweeper-vertical-slice/smoke/ui_probe.gd b/prototypes/dungeon-sweeper-vertical-slice/smoke/ui_probe.gd index b60de633..999bb7e9 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/smoke/ui_probe.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/smoke/ui_probe.gd @@ -106,8 +106,17 @@ func _ready() -> void: await get_tree().process_frame if main.board_view.board() == null: problems.append("L2 单场未启动(board=null)") - elif not main.board_view.board().variants.is_empty(): - problems.append("L2 板出现变体格(地形变异 清单为空 → 零布置)") + else: + # 地形变异 首批类型定稿批(2026-09-19):L2 板允许带变异(岩浆/烬覆)——布置域断言 = + # 变异格全在初始展开区外(closed_only 结构保障) + var b0 = main.board_view.board() + var r0p := {} + for c in b0.compute_r0(): + r0p[c] = true + for c in b0.variants.keys(): + if r0p.has(c): + problems.append("L2 板变异格落在初始展开区(地形变异 布置域违例)") + break main.board_view.set_desaturated(true) main.board_view.set_desaturated(false) if problems.is_empty():