feat(terrain-variation): 地形变异首批类型定稿批——四类型两轴×两关落地(台账 92)

- 首批四类型(2026-09-19/20 用户拍板):荆棘丛/藤蔓缠绕(L1 dungeon 平原密林)+ 岩浆/烬覆(L2 underdark 幽暗地域);伤害型=挖开轮末结算扣血(1/2 点),信息型=呈现层数字遮蔽
- 机制枚举化:effect/trigger/placement 三枚举 + 跨字段互锁(schema 重写),新增类型=加行不改代码
- 布置段移排战斗初始化第 4 步(生成棋盘→打开状态初始化→资源格→变异格,用户拍板顺序):_place_variants 数量=5%+每关+2、连通片≤4、安全格减展开区减雷格、每图单类型、variant_place 新封闭流
- biome_tags→board_types 更名(用户裁定对齐 board_type 词表);counter_items 字段位预留归 Alpha 反制批
- 冒烟 353 项全绿(+22 项变异断言);四场景探针全过;600 局×2 关模拟读数 L1 91.3/93.3、L2 78.3/81.7,承伤带全过
- 文档收口:地形变异 v0.3、棋盘生成四阶段化、战斗结算随机消费六处、registry variation_table/variant_pool_gate/rng_service、06 清单四条目收口+两条目改排 Alpha、00/02/03/05 过期提法清理
This commit is contained in:
DKChan committed 2026-09-20 01:08:14 +08:00
1 parent 87fec0278a
commit ce47064627
20 files changed
+448 -160

No files matched your search

+1 -1
View File
@@ -34,7 +34,7 @@
| 标错 | **控制失败的失控反噬**——机制上的随机读作"失控",反噬流向不可控 |
| 道具 / 奖励 | 前人探险者的遗落物(氛围背景句,登记不锁;宝箱呈现只绑定可使用道具类奖励) |
**存疑项**(未拍板,此处只登记不展开):惩罚格未来类型(陷阱/岩浆/冰冻等)只登记了名字,机制分化未定;标错反噬的具体机制落点(流向谁、一次还是每轮)未定,本文只承诺叙事壳;两类增伤分量的叙事称呼("地利/暴露")仍是候选措辞,未拍板。
**存疑项**(历史登记,此处只留痕不展开):惩罚格未来类型(陷阱/岩浆/冰冻等)——2026-09-20 首批四类型已定稿落地(荆棘丛/藤蔓缠绕/岩浆/烬覆,见[地形变异](systems/排雷循环/地形变异.md)),其余未来类型仍只登记名字;标错反噬的具体机制落点(流向谁、一次还是每轮)未定,本文只承诺叙事壳;两类增伤分量的叙事称呼("地利/暴露")仍是候选措辞,未拍板。
## 术语层级
+1 -1
View File
@@ -24,7 +24,7 @@
| `棋盘生成.md` | 棋盘怎么生成、怎么保证可推理可通关 | 已定稿·自旧体系迁移 |
| `排雷玩法.md` | 挖格、插旗、连锁展开等扫雷规则本体 | 已定稿·自旧体系迁移 |
| `行动经济.md` | 每轮行动点怎么花、停手与刷新的代价 | 已定稿·自旧体系迁移 |
| `地形变异.md` | 格子的非雾变异(岩浆/冰冻等,待定) | 已定稿·自旧体系迁移 |
| `地形变异.md` | 格子的非雾变异(首批四类型:荆棘丛/藤蔓缠绕/岩浆/烬覆) | 已定稿·首批类型落地 |
| `魔法道具.md` | 扫雷侧魔法道具(显影、探测类) | 已定稿·自旧体系迁移 |
### 战斗循环(`systems/战斗循环/`)
+2 -2
View File
@@ -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 用户拍板)
- 道具/增益之间的组合协同(遗物式联动)
- 战后自动回复——回复只有道具与恢复点两个来源
- 元进度与跨局成长
+1 -1
View File
@@ -4,7 +4,7 @@
## 确定不做(与定位冲突,永久排除)
- **雾格 / 迷雾类战争迷雾机制**:曾在旧设计中承担地形变异,2026-09-14 已从设计中移除。它给推理信息源的代价大于收益,地形变异的未来只走非雾变异(岩浆/冰冻等,待定)。
- **雾格 / 迷雾类战争迷雾机制**:曾在旧设计中承担地形变异,2026-09-14 已从设计中移除。它给推理信息源的代价大于收益,地形变异只走非雾变异(首批四类型 2026-09-20 落地)。
- **多人 / 在线要素**:游戏前提就是单机单人。合作、对战、排行榜这类在线功能会把开发成本翻倍,还不服务核心受众。
- **重叙事**:不写前人日志、剧情演出这类长篇文本。世界观只做碎片点缀,卖点在玩法不在文本。
- **即时操作玩法**:所有决策都在回合内慢慢想,不考反应、不考手速。任何"限时点击""走位闪避"都免谈。
+5 -36
View File
@@ -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 豁免)且注记「未建表零代码影响」,切片无消费方(元进度未实现),建账归冒险结构定稿批
@@ -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 类行型;新增或下线行型必须走本文修订并同步呈现层行格式化,未登记行型不得落行。
+63 -48
View File
@@ -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;岩浆/冰冻旧占位名退役(冰冻退役、岩浆落位第二关) |
+16 -20
View File
@@ -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。
- 无开局白给板:开局免费展开域 ≠ 全部安全格,违例尝试弃用重试。
@@ -138,7 +138,7 @@
| P1 | 道具使用的动效与音频反馈 | 表现打磨项后置;音频排 Alpha |
| P1 | "买情报"闭环教学与"真视不是保护"口径 | 归教学引导,Alpha 里程碑,非阻塞 |
| P1 | 信息类道具的战斗当量入战斗结算效果对照表 | 间接效应的定量换算,调参期扩列,MVP 不入表 |
| P2 | 地形变异新类型(岩浆/冰冻等)的反制道具 | 随变异建行批,按框架契约每变异至少一件 |
| P2 | 地形变异新类型的反制道具 | 归 Alpha 反制批(2026-09-20 用户拍板)——按框架契约每变异至少一件;首批四类型(荆棘丛/藤蔓缠绕/岩浆/烬覆)已建行,counter_items 待回填 |
## 调参表
+19 -17
View File
@@ -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
@@ -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":
@@ -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
@@ -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)
@@ -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
@@ -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/布置参数均为加法核小整数轴)。
@@ -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":
@@ -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
@@ -0,0 +1 @@
uid://cc2yhv5d20gfk
@@ -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:
@@ -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():