refactor(slice): 雾格机制整体清理(代码/数据表/文档/registry)

雾格已于 2026-09-14 从设计中永久移除(05-不做清单),本批把残留从实现到文档清干净。

代码与数据表:
- 删:雾布置管线(区域生长/厚度约束/safe_nonzero 强制)、雾预算核算、雾指标输出
  (actual_fog_ratio / fog_burden_ratio / delta_budget_fog)、求解器"雾元知识"信息通道、
  硬编码 "fog" 变异、雾池加载护栏、雾格渲染/tint/雾区全开信号、雾图例条目、雾首遇提示、
  雾教学断言;variation/board_gen/reward_config 三表雾字段与雾校验
- 删 inferability_core(雾移除后恒等于主指标,留着会制造双判据假象)及其 3 处消费方
- 留:变异框架面(variants/variant_ids/候选池门控/变异表加载通道与通用字段/
  出生区无变异不变式/事件载荷 variant_ids 空槽),MVP 下恒空;加行即生效承诺不变
- 验证:冒烟 196/196(+3 变异框架面断言)、三场景探针全 PASS、
  期望扫描与清理前同种子逐位相同(L2 板去雾后可推理率 0.908→0.951)

文档与 registry:
- 十份系统文档的雾失义登记与待拍板条目按删除概念 SOP 关闭并补版本历史;03/02 顶层同步
- design/ux/battle.md 与 interaction-patterns.md 去雾(格态收窄两态)+ 修订记录表
- registry:删 t_fog_min / fog_region_count / fog_target_ratio / terrain_variation_acceptance_mapping
  / pricing_constraints 等键;指标集八项→四项;hint_id 去 fog_first;
  variation_table 反向边收敛为三个真实引用方
- 连带:art-bible 雾资产占位、tests/README 示例测试名、doc-conventions 第 4 节正例、
  研究件失效注记、切片 README 现状陈述与补记
This commit is contained in:
DKChan committed 2026-09-15 21:10:27 +08:00
1 parent 9f1507dcca
commit 88a204635f
47 files changed
+232 -545

No files matched your search

@@ -163,7 +163,7 @@ Godot_v4.7.2-stable_win64_console.exe --headless --path . --script res://smoke/s
- **奖励点随机主动道具的体感**:roll 池 = 小刀 8 / 精工短刀 16 / 治疗包 12(等权);到手的刀是否改变了 Boss 战规划(囤刀 vs 即用)。
- **恢复点(30% max_hp)时机**:位于战4 与 Boss 之间——是否制造"要不要省治疗包给 Boss"的决策。
- **Boss 战节拍**:遗迹守卫环 [attack,attack,charge,heavy]——蓄力拍是否被玩家用作"安全挖掘拍";重击 14 点是否构成规划压力。
- 节点进度条/奖励恢复覆盖层可读性;v1 遗留观测项(结算逐行归因、雾池体感、完美排雷可达性)继续有效。
- 节点进度条/奖励恢复覆盖层可读性;v1 遗留观测项(结算逐行归因、完美排雷可达性)继续有效(原"雾池体感"一项随雾格移除作废)。
## 2026-09-11 用户拍板(四条,已落地本切片)
@@ -250,18 +250,20 @@ Godot_v4.7.2-stable_win64_console.exe --headless --path . --script res://smoke/s
**背景**:`design/ux/battle.md`(/ux-review APPROVED)+ `design/ux/interaction-patterns.md`(核心 8 pattern)落地到切片呈现层。**core/ 与 data/ 零改动**(本批纯 presentation/ + smoke/)。
**五区布局**(1280×800 基座):A 区 28px 顶栏关卡进度条(`level_strip_view.gd` 改造:暗金已过/奖励金当前高亮);B 区 252px 战斗画面(`battle_stage_view.gd` 新建 = `hero_card_view.gd` + 轮次·阶段指示 + 兑现演出区 + `enemy_card_view.gd` ×1–3〔顺位徽标/意图徽标常驻/环点拍数/伏击共享池/死亡灰化+碎裂残位禁瞬消〕+ 底部结算明细带);C 区 240px `hero_info_panel_view.gd`(直角细线数据框 + 修正来源角标 + RAP 点阵 + `status_slot_row.gd` ×4〔形状+色相双通道/层数角标/剩余结算点/悬停 tooltip 来源+语义+剩余/空态槽隐藏〕);D 区 `board_zone_view.gd`(`ap_bar_view.gd` 44px〔AP 点阵+停手居左;图例+弃图刷新居右——确认 chips 异位 pattern 7〕+ `board_view.gd` 改造〔悬停三语义自绘光标/目标格边框/BFS 路径蓝点预览/可挖域 8 向呼吸微光/奖励徽章 R₀ 域外/已开雾格 tint 残留/雾区全开一次性信号/去饱和 shader〕+ `toast_stack.gd` 2.5s 消退 + `board_fx_view.gd` 演出层)+ `desaturation.gdshader`(去饱和+金橙偏移、明度不变);E 区 280px `inventory_panel_view.gd`(常读列表固定槽高 + 原位窗口态 pattern 2/8:描边金光/标题切换/时机指示/usable_now 禁选+原因/跳过/政策注记;属性简述投影、来源不呈现)。**旧 `hud_view.gd` 删除**(HUDView 职责由 B/C/D 三区视图承接)。
**五区布局**(1280×800 基座):A 区 28px 顶栏关卡进度条(`level_strip_view.gd` 改造:暗金已过/奖励金当前高亮);B 区 252px 战斗画面(`battle_stage_view.gd` 新建 = `hero_card_view.gd` + 轮次·阶段指示 + 兑现演出区 + `enemy_card_view.gd` ×1–3〔顺位徽标/意图徽标常驻/环点拍数/伏击共享池/死亡灰化+碎裂残位禁瞬消〕+ 底部结算明细带);C 区 240px `hero_info_panel_view.gd`(直角细线数据框 + 修正来源角标 + RAP 点阵 + `status_slot_row.gd` ×4〔形状+色相双通道/层数角标/剩余结算点/悬停 tooltip 来源+语义+剩余/空态槽隐藏〕);D 区 `board_zone_view.gd`(`ap_bar_view.gd` 44px〔AP 点阵+停手居左;图例+弃图刷新居右——确认 chips 异位 pattern 7〕+ `board_view.gd` 改造〔悬停三语义自绘光标/目标格边框/BFS 路径蓝点预览/可挖域 8 向呼吸微光/奖励徽章 R₀ 域外/去饱和 shader〕+ `toast_stack.gd` 2.5s 消退 + `board_fx_view.gd` 演出层)+ `desaturation.gdshader`(去饱和+金橙偏移、明度不变);E 区 280px `inventory_panel_view.gd`(常读列表固定槽高 + 原位窗口态 pattern 2/8:描边金光/标题切换/时机指示/usable_now 禁选+原因/跳过/政策注记;属性简述投影、来源不呈现)。**旧 `hud_view.gd` 删除**(HUDView 职责由 B/C/D 三区视图承接)。
**全屏 States 与模态**:`legend_overlay.gd`(图例,「迷雾 = 无雷盲区」恒在 + 状态槽空态说明)/ `settlement_review_overlay.gd`(明细回看)/ `battle_end_overlay.gd`(胜=金蜡封落于棋盘中央+判定行+回看入口;败=暮蓝 #5A6C8C+旗化光点收回)——统一模态登记序管理(`_open_modal` 打开时显式隐藏上方遮挡层,缺陷⑤纪律)。清图金粒喷泉+彩纸 / 完美排雷金光带+横幅 / 标中旗加重弹道 / Boss 紫赤影子爬棋盘 / 零合法板诚实 toast / 雾首遇·chord·非相邻一次性提示(每 run 去重,`ux_hint_shown` 遥测)。**发现并修复历史 quirk**:TunePanel 默认 `visible=true` 靠被不透明开始层遮蔽的树序巧合——已显式初始化 false(缺陷⑤家族)。
**全屏 States 与模态**:`legend_overlay.gd`(图例,状态槽空态说明)/ `settlement_review_overlay.gd`(明细回看)/ `battle_end_overlay.gd`(胜=金蜡封落于棋盘中央+判定行+回看入口;败=暮蓝 #5A6C8C+旗化光点收回)——统一模态登记序管理(`_open_modal` 打开时显式隐藏上方遮挡层,缺陷⑤纪律)。清图金粒喷泉+彩纸 / 完美排雷金光带+横幅 / 标中旗加重弹道 / Boss 紫赤影子爬棋盘 / 零合法板诚实 toast / chord·非相邻一次性提示(每 run 去重,`ux_hint_shown` 遥测)。**发现并修复历史 quirk**:TunePanel 默认 `visible=true` 靠被不透明开始层遮蔽的树序巧合——已显式初始化 false(缺陷⑤家族)。
**遥测**(playtest_logger 增 ux 行族,呈现层观测不改 core):`ux_round_input_ratio`(原始输入:AP 消耗,快照驱动)/ `ux_round_exit{path_tag}`(快照 round_facts.path——呈现层只订 B1 的 ADR 纪律下唯一合法源)/ `ux_refresh_attempt{result, round_refresh_index}` / `ux_window_choice` / `ux_reject{source, sub_reason}` / `ux_hint_shown{hint_id}`。
**探针**:ui_probe 增五区构建断言 + L2 雾图段(雾首遇提示/雾区连通分量);ui_click_probe 增刷新确认链/图例开关/tooltip 触发/模态遮挡断言(图例/回看/窗口态/终局)/窗口期去饱和断言/首格可点计时(进入战斗屏口径 133–151ms ≤500ms AC;场景冷启动 ~700ms 为脚本首载编译域,观测项)——既有全部断言保留。** Waiting 刷新代理已按 OQ12 退役**:UI 刷新按钮仅挖掘期 Active 受理(presenter 门禁),原探针"窗口期点刷新"行为由 Active 确认链测试替代。
**探针**:ui_probe 增五区构建断言;ui_click_probe 增刷新确认链/图例开关/tooltip 触发/模态遮挡断言(图例/回看/窗口态/终局)/窗口期去饱和断言/首格可点计时(进入战斗屏口径 133–151ms ≤500ms AC;场景冷启动 ~700ms 为脚本首载编译域,观测项)——既有全部断言保留。** Waiting 刷新代理已按 OQ12 退役**:UI 刷新按钮仅挖掘期 Active 受理(presenter 门禁),原探针"窗口期点刷新"行为由 Active 确认链测试替代。
**回归**:smoke_all 149/149 · ui_probe PASS · ui_tune_probe PASS · ui_click_probe PASS(PROBE_SEED 7/11/23/99/42/77 六种子 done=true、选用失效=0、问题=0)。
**占位登记**:敌人意图视觉映射按 enemy_id 程序化占位(资产定稿归 B5);D区演出(喷泉/彩纸/弹道/影子)全部程序化占位;CJK 字体依赖系统回退(Windows 可用,随包字体归 D1 正式化硬性前置——ADR-0003);金蜡封/暮蓝氛围为色块占位级。
**2026-09-15 补记(雾格移除)**:雾格机制已整体移除(见[不做清单](../../design/gdd/05-不做清单.md))——本节五区规格里的已开雾格 tint 残留、雾区全开一次性信号、图例「迷雾 = 无雷盲区」条目与雾首遇提示已随机制同批从实现与探针中删除(现行格态收窄为已开 / 未开两态,一次性提示只余 chord 与非相邻两项)。本文以上各历史批次条目保留当时陈述,现行实现与呈现义务以[战斗屏规格](../../design/ux/battle.md)与 [HUD框架](../../design/gdd/systems/表现与支撑/HUD框架.md)等现行文档为准。
## 2026-09-12 呈现层 code-review 修复批(/code-review CHANGES REQUIRED → 全项清偿)
**触发**:全切片 53 脚本 ~9400 行 /code-review(CLAUDE.md 代码规范 + ADR-0001/0003/0004 对照),裁决 CHANGES REQUIRED(7 项必改)。core/ 12 文件与 smoke 断言面整体健康;病灶集中于 tune_panel 封装破口、main.gd 结构回涨、三处呈现契约错位。
@@ -304,7 +306,7 @@ Godot_v4.7.2-stable_win64_console.exe --headless --path . --script res://smoke/s
**组织修复(P1/P2/P3)**:
- **O1/O2 调试模式抽离**:main.gd 调试分支(单场/连续两层/HP 携带/共享 B4)拆 `presentation/debug_session.gd`——调试语义单一实现点(O2 双实现漂移收口);组合根保留接线(battle_created 先于 begin_battle 发出,ADR-0001 接线唯一归组合根 + 缺陷①时序义务)与终局 UI 决策。ui_probe 抓获初版时序缺陷(battle_ready 才接手 b1 → 雾首遇提示丢失)——即缺陷①同款,battle_created 接手修复。
- **O1/O2 调试模式抽离**:main.gd 调试分支(单场/连续两层/HP 携带/共享 B4)拆 `presentation/debug_session.gd`——调试语义单一实现点(O2 双实现漂移收口);组合根保留接线(battle_created 先于 begin_battle 发出,ADR-0001 接线唯一归组合根 + 缺陷①时序义务)与终局 UI 决策。ui_probe 抓获初版时序缺陷(battle_ready 才接手 b1)——即缺陷①同款,battle_created 接手修复。
- **O3 DamageMath 抽取**:B1 伤害公式族(F2 乘区/充能分量,零状态纯算术)拆 `core/damage_math.gd`(规范 1.2 工具函数挂宿主类);board_generator 同一合法板重复调用两次 measure_inferability 合并为一次(全库最贵求解器)。
- **P3 清债**:头注溯源补齐 ×4(board_data/config_db/rng_service/reward_supply_controller——规范 1.4);B4 grant/grant_clear/stock/use 失败返回统一 `false`→`null`(E6 消费方 `is Dictionary` 判定天然兼容);equivalence_supply 表行 duplicate 防配置源泄漏;load()→preload/类名直引 ×3(main logger/runner、tune_sim autoplay)。
@@ -147,7 +147,7 @@ func _install_new_board(during_round := false) -> bool:
for c in board.safe_cells():
if r0.has(c):
continue
eligible.append({"cell": c, "fog": board.variant_ids(c).size() > 0})
eligible.append({"cell": c})
if not _reward_economy.roll_board(eligible):
_fatal("奖励道具经济 roll_board 被拒")
return false
@@ -86,14 +86,6 @@ func safe_cells() -> Array:
return out
func fog_cells() -> Array:
var out: Array = []
for c in all_cells():
if not variants.get(c, []).is_empty():
out.append(c)
return out
# R₀ = 出生 3×3 ∪ (出生 3×3 经 0 格的 flood 闭包)——与 排雷玩法 F3 逐字同构(棋盘生成 F1 求解器初始态)。
func compute_r0() -> Array:
var b3 := spawn_cells()
@@ -109,15 +101,3 @@ func compute_r0() -> Array:
r0[n] = true
queue.append(n)
return r0.keys()
# 棋盘生成 Rule 4 排除域(以最终产物板计):R₀ ∪ 外扩 1 环——生成期确定性计算。
func exclusion_domain() -> Dictionary:
var r0 := compute_r0()
var dom := {}
for c in r0:
dom[c] = true
for c in r0:
for n in neighbors8(c):
dom[n] = true
return dom
@@ -1,8 +1,8 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
# Date: 2026-09-10
# 确定性棋盘生成器:五阶段管线(参数→布雷→变异→验证循环→产出)+ F1 可推理率度量
# (single_subset 求解器)+ F3 雾预算 + F5 验收算子(达标即出/接受最优/接受最不坏)。
# 确定性棋盘生成器:五阶段管线(参数→布雷→变异布置→生成-验证→产出)+ F1 可推理率度量
# (single_subset 求解器)+ F5 验收算子(达标即出/接受最优/接受最不坏)。
# 切片裁剪登记:黄金向量回归套件与 F4 批量工具不做(难度曲线与Boss 调参期工具域)。
# 实现 GDD:design/archive/gdd-2026-09-14/a1-board-generation.md(系统 ID A1·棋盘生成——registry 追溯锚,命名规范见 CLAUDE.md)
class_name BoardGenerator
@@ -28,8 +28,6 @@ func generate(L: int, D: int, rng_stream: RandomNumberGenerator, db) -> BoardDat
"solver_rules": String(row.solver_rules),
"min_safe": int(row.min_safe_count),
"max_attempts": int(row.max_gen_attempts),
"fog_count": int(row.fog_region_count),
"fog_ratio": float(row.fog_target_ratio),
"biomes": Array(row.board_biomes),
}
var best_legal: BoardData = null
@@ -111,12 +109,11 @@ func _attempt(params: Dictionary, rng_stream: RandomNumberGenerator, db) -> Boar
b.mines.erase(c)
removed += 1
_compute_digits(b)
# ③ 变异布置(先雷后雾;排除域以最终产物板计)
var shortfall := _place_variants(b, params, rng_stream, db)
# ③ 变异布置(地形变异 框架面):布置约束与算法随首批变异类型定稿(生成期布置接口结构位),
# 定稿前零布置——清单为空是合法态(P0),静默产出无变异图。
b.metrics = {
"metric_version": METRIC_VERSION,
"ring_removed": removed,
"shortfall": shortfall,
}
return b
@@ -132,99 +129,13 @@ func _compute_digits(b: BoardData) -> void:
b.digits[c] = d
# 变异布置:候选池门控 + cell_constraint 生成期强制 + 排除域排除 + 厚度≤2;返回 shortfall
func _place_variants(b: BoardData, params: Dictionary, rng_stream: RandomNumberGenerator, db) -> int:
if params.fog_count <= 0 or params.fog_ratio <= 0.0:
return 0
var pool: Array = db.variant_pool(params.L, params.biomes)
if pool.is_empty():
return 0 # EC1 空池静默——合法非错误
var exclusion := b.exclusion_domain()
var total_target := int(round(params.fog_ratio * float(params.w * params.h)))
var placed_total := 0
var achieved := 0
var placed := {}
for r in range(params.fog_count):
var region_target: int = total_target / params.fog_count
if r < total_target % params.fog_count:
region_target += 1
if region_target <= 0:
achieved += 1 # 零目标区视为达成(不消耗布置)
continue
var region := _grow_region(b, region_target, exclusion, placed, rng_stream)
if region.is_empty():
continue # EC2:无种子 → 该区布置失败(shortfall)
for c in region.keys():
b.variants[c] = ["fog"]
placed[c] = true
placed_total += 1
if region.size() >= region_target:
achieved += 1
return params.fog_count - achieved
# 区域生长:随机种子 + 随机边界扩张(连通域);厚度≤2 = 任意 3×3 窗口非全雾
func _grow_region(b: BoardData, target: int, exclusion: Dictionary, placed: Dictionary, rng_stream: RandomNumberGenerator) -> Dictionary:
var candidates: Array = []
for c in b.all_cells():
if _valid_fog_cell(b, c, exclusion, placed):
candidates.append(c)
if candidates.is_empty():
return {}
var region := {}
var seed_c: Vector2i = candidates[rng_stream.randi_range(0, candidates.size() - 1)]
region[seed_c] = true
while region.size() < target:
var frontier: Array = []
for c in region.keys():
for n in b.neighbors8(c):
if not region.has(n) and not placed.has(n) and _valid_fog_cell(b, n, exclusion, placed) and not _breaks_thickness(region, n, b):
frontier.append(n)
if frontier.is_empty():
break # 尽力布置——部分区域合法(shortfall 上报)
var pick: Vector2i = frontier[rng_stream.randi_range(0, frontier.size() - 1)]
region[pick] = true
return region
# cell_constraint=safe_nonzero 生成期强制:非雷、非 0(⟺ 8 邻含雷)、不在排除域
func _valid_fog_cell(b: BoardData, c: Vector2i, exclusion: Dictionary, placed: Dictionary) -> bool:
if placed.has(c) or exclusion.has(c):
return false
if not b.is_safe(c):
return false
if b.digit_of(c) <= 0:
return false
return true
func _breaks_thickness(region: Dictionary, candidate: Vector2i, b: BoardData) -> bool:
# 加入 candidate 后是否出现全雾 3×3 窗口
for dy in range(-1, 2):
for dx in range(-1, 2):
var origin := Vector2i(candidate.x + dx - 1, candidate.y + dy - 1)
var all_fog := true
for wy in range(3):
for wx in range(3):
var c := Vector2i(origin.x + wx, origin.y + wy)
var is_fog := (region.has(c) and c != candidate) or c == candidate
if not is_fog:
all_fog = false
if all_fog:
return true
return false
# ==================== F1 可推理率度量(独立入口——与内嵌调用同一实现) ====================
# 返回 {r0, r_inf, inferability, inferability_core}
# 返回 {r0, r_inf_count, inferability}
func measure_inferability(b: BoardData) -> Dictionary:
var safe_set := {}
for c in b.safe_cells():
safe_set[c] = true
var fog_set := {}
for c in b.fog_cells():
fog_set[c] = true
var r := {}
for c in b.compute_r0():
r[c] = true
@@ -232,23 +143,15 @@ func measure_inferability(b: BoardData) -> Dictionary:
var changed := true
while changed:
changed = false
# 算子① 连锁:0 格的 8 邻安全格全入 R(雾格可被链开但不传播——雾恒非 0,天然为叶)
# 算子① 连锁:0 格的 8 邻安全格全入 R
var frontier: Array = r.keys()
for x in frontier:
if safe_set.has(x) and not fog_set.has(x) and b.digit_of(x) == 0:
if safe_set.has(x) and b.digit_of(x) == 0:
for n in b.neighbors8(x):
if safe_set.has(n) and not r.has(n):
r[n] = true
changed = true
# 信息源(b)雾元知识:与 R 邻接的雾格必非雷 → 入 R
for f in fog_set.keys():
if not r.has(f):
for n in b.neighbors8(f):
if r.has(n):
r[f] = true
changed = true
break
# 信息源(a)数字约束:single + subset(solver_rules=single_subset 冻结域)
# 信息源:数字约束 single + subset(solver_rules=single_subset 冻结域)
var constraints := _build_constraints(b, r, known_mines, safe_set)
# single 规则
for con in constraints:
@@ -286,28 +189,19 @@ func measure_inferability(b: BoardData) -> Dictionary:
changed = true
var r_inf := r.keys()
var s_count := safe_set.size()
var s_nonfog := 0
var r_nonfog := 0
for c in safe_set.keys():
if not fog_set.has(c):
s_nonfog += 1
if r.has(c):
r_nonfog += 1
var inferability: float = float(r_inf.size()) / float(maxi(1, s_count))
var core: float = float(r_nonfog) / float(maxi(1, s_nonfog))
return {
"r0": b.compute_r0().size(),
"r_inf_count": r_inf.size(),
"inferability": inferability,
"inferability_core": core,
}
func _build_constraints(b: BoardData, r: Dictionary, known_mines: Dictionary, safe_set: Dictionary) -> Array:
var out: Array = []
for c in r.keys():
if not safe_set.has(c) or b.variant_ids(c).size() > 0:
continue # 雾格数字不可见(hide_numbers)——零特判数据面:变体格不产约束
if not safe_set.has(c):
continue # 只对已开安全格的数字建约束
var d := b.digit_of(c)
var unknown := {}
var known_here := 0
@@ -349,40 +243,6 @@ func _diff_constraint(big: Dictionary, small: Dictionary) -> Dictionary:
return {"mines": big.mines - small.mines, "unknown": diff}
# ==================== F3 雾预算(独立入口) ====================
func measure_fog_budget(b: BoardData, dig_ap_cost: int) -> Dictionary:
var safe_set := {}
for c in b.safe_cells():
safe_set[c] = true
# Chain₀ = R₀ 经算子①单独闭包
var chain0 := {}
for c in b.compute_r0():
chain0[c] = true
var queue: Array = chain0.keys()
while not queue.is_empty():
var c: Vector2i = queue.pop_front()
if safe_set.has(c) and b.digit_of(c) == 0 and b.variant_ids(c).is_empty():
for n in b.neighbors8(c):
if safe_set.has(n) and not chain0.has(n):
chain0[n] = true
queue.append(n)
var u_count := 0
for c in b.fog_cells():
if not chain0.has(c):
u_count += 1
var pay := 0
for c in safe_set.keys():
if not chain0.has(c):
pay += 1
return {
"chain0_count": chain0.size(),
"u_count": u_count,
"delta_budget_fog": u_count * dig_ap_cost,
"fog_burden_ratio": float(u_count) / float(maxi(1, pay)),
}
# ==================== 合法性闸(独立入口;F5 legality + R₀≠S 修订义务) ====================
func is_clearable(b: BoardData) -> bool:
@@ -438,18 +298,6 @@ func _legality_faults(b: BoardData, params: Dictionary) -> Array:
var min_safe: int = int(params.get("min_safe", params.get("min_safe_count", 0)))
if b.safe_cells().size() < min_safe:
faults.append("min_safe_short")
# 雾结构:cell_constraint / 厚度 / f≤8d / 排除域
var fog := b.fog_cells()
var exclusion := b.exclusion_domain()
for c in fog:
if b.is_mine(c) or b.digit_of(c) <= 0:
faults.append("fog_cell_constraint")
if exclusion.has(c):
faults.append("fog_in_exclusion")
if _has_all_fog_window(b, fog):
faults.append("fog_thickness")
if fog.size() > 8 * b.mines.size():
faults.append("fog_over_8d")
return faults
@@ -461,49 +309,25 @@ func _neighbor_mine_count(b: BoardData, c: Vector2i) -> int:
return d
func _has_all_fog_window(b: BoardData, fog: Array) -> bool:
var fog_set := {}
for c in fog:
fog_set[c] = true
for y in b.height:
for x in b.width:
var all_fog := true
for wy in range(3):
for wx in range(3):
var c := Vector2i(x + wx, y + wy)
if not b.in_bounds(c) or not fog_set.has(c):
all_fog = false
if all_fog:
return true
return false
# ==================== 产出上报(生成期指标集) ====================
func _finalize(b: BoardData, params: Dictionary, attempts_used: int, accept_mode: String, db) -> void:
func _finalize(b: BoardData, params: Dictionary, attempts_used: int, accept_mode: String, _db) -> void:
var m := measure_inferability(b)
var fog_budget := measure_fog_budget(b, db.action_economy.dig_ap_cost)
var fog := b.fog_cells()
# 产物断言(棋盘生成 出生区安全与反退化布雷):实际雷数 ≤ 目标雷数 N——先布后删只减不增,失败即致命
var n_target: int = int(round(float(params.density) * float(params.w * params.h - 9)))
if b.mines.size() > n_target:
_fatal("产物断言违例:实际雷数 %d > 目标雷数 N=%d(先布后删只减不增)" % [b.mines.size(), n_target])
var attempt_facts := b.metrics # 保留尝试期事实(ring_removed / shortfall)
var attempt_facts := b.metrics # 保留尝试期事实(ring_removed)
b.metrics = {
"metric_version": METRIC_VERSION,
"inferability": m.inferability,
"inferability_core": m.inferability_core,
"actual_fog_ratio": float(fog.size()) / float(b.width * b.height),
"actual_mines": b.mines.size(),
"target_mines": n_target,
"actual_safe_count": b.safe_cells().size(),
"fog_burden_ratio": fog_budget.fog_burden_ratio,
"delta_budget_fog": fog_budget.delta_budget_fog,
"attempts_used": attempts_used,
"accept_mode": accept_mode,
"degraded_product": accept_mode == "accept_best",
"illegal_product": accept_mode == "accept_worst",
"shortfall": attempt_facts.get("shortfall", 0),
"ring_removed": attempt_facts.get("ring_removed", 0),
"r0_count": m["r0"],
}
@@ -122,8 +122,7 @@ func _cross_validate(loaded: Dictionary) -> void:
var eff: String = row.get("effect_id", "")
if eff != "" and not effect_ids.has(eff):
push_diag("error", "dangling_effect", "RewardTable %s 引用不存在的 effect_id: %s" % [row.get("reward_id"), eff])
# ② 池空护栏:safe_cell × 雾/非雾两态各 ≥1 活行;活 buff 行集非空
var t_fog: int = rc.t_fog_min
# ② 池空护栏:safe_cell 池须 ≥1 活行;活 buff 行集非空
var safe_rows: Array = []
var clear_rows: Array = []
for row in rt.rows:
@@ -133,15 +132,8 @@ func _cross_validate(loaded: Dictionary) -> void:
safe_rows.append(row)
elif row.get("pool_tag", "") == "clear_default":
clear_rows.append(row)
var nonfog_pool := safe_rows
var fog_pool: Array = []
for row in safe_rows:
if int(row.get("tier", 0)) >= t_fog:
fog_pool.append(row)
if nonfog_pool.is_empty():
push_diag("error", "pool_empty_safe", "safe_cell 非雾池无活行")
if fog_pool.is_empty():
push_diag("error", "pool_empty_fog", "safe_cell 雾态池无活行(tier ≥ t_fog_min)")
if safe_rows.is_empty():
push_diag("error", "pool_empty_safe", "safe_cell 池无活行")
var has_buff := false
for row in safe_rows:
if row.get("category", "") == "buff":
@@ -94,7 +94,7 @@ func install_board(b: BoardData, during_round := false) -> Dictionary:
_spawn = {}
for c in b.spawn_cells():
_spawn[c] = true
# 初始展开:O₀ = R₀ 恰等(免费/计入清图/无奖励/永不触雷触雾);板级一次跨轮不重放
# 初始展开:O₀ = R₀ 恰等(免费/计入清图/无奖励/永不触雷);板级一次跨轮不重放
for c in _initial_expansion_order(b):
_revealed[c] = {"kind": "safe", "digit": b.digit_of(c)}
cell_revealed.emit({
@@ -157,7 +157,7 @@ func _initial_expansion_order(b: BoardData) -> Array:
var queue: Array = order.duplicate()
while not queue.is_empty():
var c: Vector2i = queue.pop_front()
if b.is_safe(c) and b.digit_of(c) == 0 and b.variant_ids(c).is_empty():
if b.is_safe(c) and b.digit_of(c) == 0:
for n in b.neighbors8(c):
if b.is_safe(n) and not seen.has(n):
seen[n] = true
@@ -248,7 +248,7 @@ func _do_dig(c: Vector2i) -> Dictionary:
# 资源面/相位 + cleared 仲裁在 行动经济 事务内原子完成
if not action_economy.request_dig(chain_size, clears):
return _outcome(false, "rejected", "resource_face", "")
# 事务内执行:首格 + 连锁新格揭示(BFS 规范序,含链开雾格——零特判);事件 = O′ ∖ O
# 事务内执行:首格 + 连锁新格揭示(BFS 规范序,零特判);事件 = O′ ∖ O
var reveal_order: Array = _chain_reveal_order(c, _flags)
for rc in reveal_order:
if _revealed.has(rc):
@@ -290,7 +290,7 @@ func _chain_closure(start: Vector2i, flag_set: Dictionary) -> Dictionary:
var queue: Array = [start]
while not queue.is_empty():
var c: Vector2i = queue.pop_front()
if _board.is_safe(c) and not flag_set.has(c) and _board.digit_of(c) == 0 and _board.variant_ids(c).is_empty():
if _board.is_safe(c) and not flag_set.has(c) and _board.digit_of(c) == 0:
for n in _board.neighbors8(c):
if _board.is_safe(n) and not flag_set.has(n) and not chain.has(n):
chain[n] = true
@@ -305,7 +305,7 @@ func _chain_reveal_order(start: Vector2i, flag_set: Dictionary) -> Array:
var queue: Array = [start]
while not queue.is_empty():
var c: Vector2i = queue.pop_front()
if _board.is_safe(c) and not flag_set.has(c) and _board.digit_of(c) == 0 and _board.variant_ids(c).is_empty():
if _board.is_safe(c) and not flag_set.has(c) and _board.digit_of(c) == 0:
for n in _board.neighbors8(c):
if _board.is_safe(n) and not flag_set.has(n) and not seen.has(n):
seen[n] = true
@@ -17,7 +17,7 @@ var rng
var _supply: RewardSupplyController # 可选类供给状态机(组合件——战斗域计数)
var _phase := "unrolled" # unrolled → ready → retired;clear_inventory 后 _inventory_cleared
var _inventory_cleared := false
var _eligible: Array = [] # [{cell, fog}] 行主序规范序
var _eligible: Array = [] # [{cell}] 行主序规范序
var _mapping := {} # Vector2i → reward_id
var _power_roll := {} # Vector2i → 区间行 roll 值(伤害量值区间批——身份批内追加取号)
var _clear_power := -1 # 清图槽区间行 roll 值(-1 = 定值行)
@@ -59,7 +59,7 @@ func roll_board(eligible: Array) -> bool:
push_warning("[奖励道具经济] 资格清单重复格已去重: %s" % str(c))
continue
seen[c] = true
list.append({"cell": c, "fog": bool(e.get("fog", false))})
list.append({"cell": c})
list.sort_custom(func(a, b): return a.cell.y * 1000 + a.cell.x < b.cell.y * 1000 + b.cell.x)
_eligible = list
_granted.clear() # 新批重置映射受理面(旧图快照消亡)
@@ -71,19 +71,15 @@ func roll_board(eligible: Array) -> bool:
var safe_rows: Array = _pool_rows("safe_cell")
var clear_rows: Array = _pool_rows("clear_default")
var inject_rows: Array = _pool_rows("active_inject")
# ① 身份批:每资格格恰一即时类身份(可选类已退出格子池——供给控制器批;雾档过滤不变)
# ① 身份批:每资格格恰一即时类身份(可选类已退出格子池——供给控制器批)
var draft := {}
for e in list:
var pool := safe_rows.filter(func(row):
if e.fog and int(row.tier) < int(db.reward_config.t_fog_min):
return false # F2 雾档过滤
return true)
draft[e.cell] = weighted_pick(pool, stream.randi_range(0, _pool_weight(pool) - 1))
draft[e.cell] = weighted_pick(safe_rows, stream.randi_range(0, _pool_weight(safe_rows) - 1))
# ② 清图槽:一次性高额伤害(当场兑付类——不入库、不占可选类供给;2026-09-11 用户拍板)
var clear_rid := weighted_pick(clear_rows, stream.randi_range(0, _pool_weight(clear_rows) - 1))
# ③ 可选类供给注入(替代旧 F5 刀保底):首件保证 / p0×decay^(j-1) 指数递减——判定恰一取号
if _supply.should_inject(stream):
var target = list[stream.randi_range(0, list.size() - 1)] # 均匀随机(拍板③——雾格不加权)
var target = list[stream.randi_range(0, list.size() - 1)] # 均匀随机(拍板③)
draft[target.cell] = weighted_pick(inject_rows, stream.randi_range(0, _pool_weight(inject_rows) - 1))
# ④ 伤害量值区间批(2026-09-14):身份定稿后逐格 roll power(区间行恰一取号,定值行零取号);
# 授予期零随机纪律不变——快照 E14 口径,战斗结算/呈现零改动
@@ -8,7 +8,6 @@ extends Resource
@export var rows: Array = [] # Array[Dictionary]
const SOLVER_RULES := ["single", "single_subset"]
const FOG_RATIO_ABS_CAP := 0.25 # 战斗结算 修订——雾占比绝对上限
const COST_BUDGET := 16384 # F2c provisional 量级护栏(禁墙钟逃生)
const TARGET_ABS_FLOOR := 0.85 # 战斗结算 修订——target 域下限(P1 绝对锚)
@@ -51,12 +50,6 @@ func validate() -> Array:
var att: int = int(row.get("max_gen_attempts", 0))
if att < 1 or att > 32:
errs.append({"category": "domain", "message": "(%d,%d) max_gen_attempts 越域 [1,32]" % [L, D]})
var fc: int = int(row.get("fog_region_count", 0))
var fr: float = float(row.get("fog_target_ratio", 0.0))
# F2 规范合取形 + 雾占比绝对上限(战斗结算 修订)
var fog_ok := (fc == 0 and fr == 0.0) or (fc >= 1 and fr > 0.0 and fr <= minf(8.0 * d, FOG_RATIO_ABS_CAP) and fr < 1.0)
if not fog_ok:
errs.append({"category": "cross_field", "message": "(%d,%d) fog_domain_ok 违例(count=%d ratio=%.3f)" % [L, D, fc, fr]})
# 布雷数下限(R3)——零雷板拒绝
var n_mines := int(round(d * float(w * h - 9)))
if n_mines < 1:
@@ -5,7 +5,6 @@
class_name RewardConfigTable
extends Resource
@export var t_fog_min: int = 2 # [1,8] F2 雾档过滤门限
@export var k_dmg: int = 0 # [0,999] 旧 F5 格池刀保底——2026-09-11 可选类供给控制器批置 0(语义由"战斗首件保证"取代)
@export var n_draft: int = 32 # [1,999] 旧 F5 草稿重 roll 上限(随 k_dmg=0 休眠——字段保留)
@export var p_min: int = 4 # [1,999] K≥1 供给闸刀力下界(随 k_dmg=0 休眠——字段保留)
@@ -15,8 +14,6 @@ extends Resource
func validate() -> Array:
var errs: Array = []
if t_fog_min < 1 or t_fog_min > 8:
errs.append({"category": "domain", "message": "t_fog_min 越域 [1,8]: %d" % t_fog_min})
if k_dmg < 0 or k_dmg > 999:
errs.append({"category": "domain", "message": "k_dmg 越域 [0,999]: %d" % k_dmg})
if n_draft < 1 or n_draft > 999:
@@ -14,10 +14,8 @@ const CELL_CONSTRAINTS := ["any", "safe_nonzero"]
func validate() -> Array:
# 空清单是合法状态(地形变异 P0)——零行即零变体格,配置加载零错误、零警告
var errs: Array = []
if rows.is_empty():
errs.append({"category": "empty_table", "message": "VariationTable 为空"})
return errs
var seen := {}
for row in rows:
var vid: String = row.get("variant_id", "")
@@ -14,8 +14,6 @@ rows = [{
"solver_rules": "single_subset",
"min_safe_count": 50,
"max_gen_attempts": 16,
"fog_region_count": 0,
"fog_target_ratio": 0.0,
"board_biomes": ["dungeon"]
}, {
"level_index": 1,
@@ -27,8 +25,6 @@ rows = [{
"solver_rules": "single_subset",
"min_safe_count": 50,
"max_gen_attempts": 16,
"fog_region_count": 0,
"fog_target_ratio": 0.0,
"board_biomes": ["dungeon"]
}, {
"level_index": 1,
@@ -40,8 +36,6 @@ rows = [{
"solver_rules": "single_subset",
"min_safe_count": 60,
"max_gen_attempts": 24,
"fog_region_count": 0,
"fog_target_ratio": 0.0,
"board_biomes": ["dungeon"]
}, {
"level_index": 1,
@@ -53,8 +47,6 @@ rows = [{
"solver_rules": "single_subset",
"min_safe_count": 60,
"max_gen_attempts": 24,
"fog_region_count": 0,
"fog_target_ratio": 0.0,
"board_biomes": ["dungeon"]
}, {
"level_index": 1,
@@ -66,8 +58,6 @@ rows = [{
"solver_rules": "single_subset",
"min_safe_count": 60,
"max_gen_attempts": 24,
"fog_region_count": 0,
"fog_target_ratio": 0.0,
"board_biomes": ["dungeon"]
}, {
"level_index": 2,
@@ -79,16 +69,14 @@ rows = [{
"solver_rules": "single_subset",
"min_safe_count": 60,
"max_gen_attempts": 24,
"fog_region_count": 1,
"fog_target_ratio": 0.10,
"board_biomes": ["dungeon"]
}]
; 切片调参登记(SLICE-TUNED provisional,调参所有权仍归 C3):
; A1 GDD 参考行(d0.10/0.12)经"先布后删"环内移除后仅幸存 2-3 雷、R0/|S|约0.97、残余挖掘 2-4 格——
; 掘近退化板(A2 反例 B 机制的低密度放大)。实验数据(8 种子均值,smoke/experiment_density.gd):
; d0.25 → L1 幸存雷 11/残余 13/inf 0.925/att 2.9 · L2 雷 19/残余 21/inf 0.908/att 7.9(seed42 实测 core 0.889→target 定 0.88 首过),零降级告警。
; d0.25 → L1 幸存雷 11/残余 13/inf 0.925/att 2.9 · L2 雷 19/残余 21/inf 0.908/att 7.9(seed42 实测 inf 0.889→target 定 0.88 首过),零降级告警。
; target 收窄至 0.88/0.88(不低于 0.85 绝对锚)为密度上移的对价——B1 条款③⑤对账重跑义务随调参触发。
; 2026-09-11 关卡扩展批:L1 增 D1–D4 行(Boss=Dmax=4)。L1 全程无雾——A4 迷雾 unlock_level=2(已锁事实),
; 2026-09-11 关卡扩展批:L1 增 D1–D4 行(Boss=Dmax=4)。
; Boss 板以 12×12 区分(d0.28/t0.87 实测达不到推理率门槛——best≈0.81 降级告警,回退 0.25/0.88;
; 密度轴上调留给 C3 与求解器能力一起解)。
@@ -34,4 +34,4 @@ rows = [{
; 节点链 = 用户 2026-09-11 拍板的线性第一关(战×2 → 随机主动道具 → 战×2 → 血量恢复点 → Boss)。
; node_depth 轴 = 节点深度(难度 = f(关卡序号, 节点深度)——game-concept 锁定口径);Boss = Dmax 行(A1 第三轴登记约定)。
; rest_heal_pct = 30:恢复点回复 max_hp 的百分比(随生命天赋同步放大,防高生命档恢复失效)。
; L1 全程无雾:A4 迷雾 unlock_level=2(已锁事实——迷雾自第二层解锁),Boss 板以 12×12 加密板区分。
; Boss 板以 12×12 加密板区分。
@@ -4,7 +4,6 @@
[resource]
script = ExtResource("1")
t_fog_min = 2
k_dmg = 0
n_draft = 32
p_min = 8
@@ -4,13 +4,8 @@
[resource]
script = ExtResource("1")
rows = [{
"variant_id": "fog",
"display_name": "迷雾",
"unlock_level": 2,
"scope": "region",
"info_mask": "hide_numbers",
"cell_constraint": "safe_nonzero",
"biome_tags": [],
"counter_item_ids": []
}]
rows = []
; 地形变异 清单为空是合法态(地形变异 P0):零变体行 → 棋盘生成零布置,行为与不存在本系统时一致。
; 首批变异类型(岩浆/冰冻,机制未定)定稿时在本清单加一行声明即应生效,无需改代码。雾格已整体
; 移除(见 design/gdd/05-不做清单.md)——本表不再有 fog 行。
@@ -37,7 +37,7 @@ func is_level_mode() -> bool:
return _level_mode
# 单场遭遇原语(2026-09-13 用户裁决:单场用户入口已删——本方法仅 ui_probe 雾板变体段使用;
# 单场遭遇原语(2026-09-13 用户裁决:单场用户入口已删——本方法仅 ui_probe 单场段使用;
# 无库存延续、standalone 终局语义保留供探针战斗收尾)
func start_single(level: int) -> bool:
_level_mode = false
@@ -55,7 +55,7 @@ var tune_panel: TunePanel
var _modals = null # 模态层级状态机(modal_stack.gd——缺陷⑤纪律载体)
var _tune_sim = null # 调整板批量模拟控制器(tune_sim_controller.gd)
var _hints_shown := {} # 一次性非模态提示去重(每 run 一次:fog_first/chord/no_adjacent)
var _hints_shown := {} # 一次性非模态提示去重(每 run 一次:chord/no_adjacent)
var _refresh_used_round := 0 # 本轮已受理刷新次数(遥测 round_refresh_index——每轮 round_started 归零)
var _boss_active := false # Boss 遭遇变体(阶段指示附 Boss 名)
var _window_payload: Array = [] # 窗口投影缓存(ux_window_choice 遥测读 usable_now/item_id)
@@ -97,7 +97,6 @@ func _build_ui() -> void:
board_zone.legend_toggled.connect(_on_legend_toggled)
board_zone.hover_forbidden.connect(_on_hover_forbidden)
board_zone.chord_attempted.connect(_on_chord_attempted)
board_zone.fog_region_completed.connect(_on_fog_region_completed)
inventory_panel.item_selected.connect(_on_window_item_selected)
inventory_panel.skipped.connect(_on_window_skipped)
inventory_panel.reject_reason.connect(_on_inventory_reject)
@@ -291,7 +290,7 @@ func _start_debug_levels() -> void:
func _on_debug_battle_created(settlement_ref, _level: int, _carry: int) -> void:
# begin_battle 前接手 + 接线(ADR-0001:signal.connect 唯一归组合根)。settlement 必须先于 begin_battle
# 就位——begin 期间发出的 board_changed 要用新板重建棋盘并触发雾首遇提示(缺陷①同款时序义务)
# 就位——begin 期间发出的 board_changed 要用新板重建棋盘并触发一次性提示(缺陷①同款时序义务)
settlement = settlement_ref
_wire_battle(settlement_ref)
@@ -414,10 +413,6 @@ func _check_board_hints() -> void:
# 接受最优降级(有合法板但无一达标)——降级标记独立于非法产物标记,同走诚实告知
elif board.degraded_product:
board_zone.toast.show_toast("本板为接受最优降级产物(未达可推理率及格线)——数据面诚实告知", "warn")
# 雾首遇(每 run 一次;必含三要点:无雷盲区不赌命 / 雾格不传播连锁 / 雾内插旗纯损——地形变异 D1×②)
if not board.fog_cells().is_empty() and not _hints_shown.has("fog_first"):
_hints_shown["fog_first"] = true
_show_hint("迷雾=无雷盲区,不赌命开雾;雾格不传播连锁;雾内插旗纯损。", "fog_first")
func _on_hover_forbidden(_cell: Vector2i) -> void:
@@ -430,11 +425,6 @@ func _on_chord_attempted(_cell: Vector2i) -> void:
_show_hint("本作无双击快开——插旗后轮末自动判定标中;点数字格可移动小人", "chord")
func _on_fog_region_completed(region_size: int) -> void:
# 雾区全开一次性完成信号(视觉合并域 = 连通分量,禁实例级双响——EC11)
board_zone.toast.show_toast("雾区已全开(%d 格)——迷雾域尽收眼底" % region_size, "gold")
func _show_hint(text: String, hint_id: String) -> void:
_hints_shown[hint_id] = true
logger.log_ux("ux_hint_shown", {"hint_id": hint_id})
@@ -1,9 +1,9 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 实现 GDD:design/ux/battle.md(战斗屏 D 区 BoardView——四格态/悬停预承诺光标/可挖域呼吸微光/
# BFS 路径预览/奖励徽章/雾区全开一次性完成信号/结算期去饱和锁输入)。
# 实现 GDD:design/ux/battle.md(战斗屏 D 区 BoardView——格态/悬停预承诺光标/可挖域呼吸微光/
# BFS 路径预览/奖励徽章/结算期去饱和锁输入)。
# Date: 2026-09-12(battle.md 五区改造批;基础 = 2026-09-11 HUD框架-lite 版)
# 棋盘视图:格子渲染(art-bible 明亮风、数字高对比、雾形状+色相双通道、
# 绘制优先级 数字>小人>旗>奖励徽章>变异纹带>底色)+ 玩家输入意图翻译 + 悬停预承诺三态。
# 棋盘视图:格子渲染(art-bible 明亮风、数字高对比、
# 绘制优先级 数字>小人>旗>奖励徽章>底色)+ 玩家输入意图翻译 + 悬停预承诺三态。
# 高内聚边界:只读 排雷玩法 查询面(+奖励道具经济 身份解析回调)做渲染与意图识别,动作经信号外发——不调用核心写接口
# (settle_batch 归组合根编排),不持久持有战斗态(minesweeper 引用随 refresh 更新,仅作只读查询)。
# 缺陷①回归约束:格子控件常驻、逐格重绘在 refresh 内完成,无布局回流(格控件/子标记定长,不增删)。
@@ -14,12 +14,9 @@ extends Control
signal action_requested(cell: Vector2i, kind: String) # kind ∈ {dig, move, flag_place, flag_pull}
signal hover_forbidden(cell: Vector2i) # 非相邻未开格悬停(首遇 toast 归组合根去重)
signal chord_attempted(cell: Vector2i) # 双击快开尝试(chord 肌肉记忆提示锚)
signal fog_region_completed(region_size: int) # 雾区全开一次性信号(视觉合并域=连通分量,EC11)
const COL_BRICK := Color8(201, 175, 131) # 未开——掩尘完整地砖(色板原语 #C9AF83)
const COL_BRICK_FOG := Color8(150, 136, 196) # 未开雾格——色相 + "雾"字双通道
const COL_FLOOR := Color8(179, 154, 120) # 已开——裸土 #B39A78(碎裂裸露地面)
const COL_FLOOR_FOG := Color8(216, 208, 234) # 已开雾格——底色层淡雾 tint 残留(OQ8 裁决;无字)
const COL_DIGIT := Color8(51, 38, 27) # 数字——墨族 #33261B(对格底 ≥4.5:1 硬指标)
const COL_HERO := Color8(46, 110, 142) # 小人边框——小人蓝 #2E6E8E
const COL_FLAG := Color8(198, 61, 34) # 旗朱 #C63D22
@@ -48,8 +45,6 @@ var _breath_t := 0.0
var _dig_glow := false # 挖掘期才允许唯一在动元素(art-bible §2.1)
var _hover := Vector2i(-9, -9)
var _last_click := {"cell": Vector2i(-9, -9), "ms": -100000}
var _fog_done := {} # 本板已完成雾区连通分量(键 = 分量最小格)
var _fog_parts: Array = [] # 本板雾区连通分量(rebuild 期计算——合并呈现域)
var _r0 := {} # 出生展开区 R₀(奖励道具经济 mapping 资格域 = S∖R₀——徽章查询跳过,防 E19 诊断噪音)
var _desat_mat: ShaderMaterial
var _desat_tween: Tween
@@ -117,15 +112,12 @@ func rebuild(board: BoardData, minesweeper) -> void:
_cells.clear()
_cell_sb.clear()
_marker.clear()
_fog_done.clear()
_fog_parts.clear()
_r0.clear()
_hover = Vector2i(-9, -9)
for c in _board.compute_r0():
_r0[c] = true
if _board == null:
return
_compute_fog_parts()
_grid.columns = _board.width
for y in _board.height:
for x in _board.width:
@@ -155,7 +147,6 @@ func refresh() -> void:
for y in _board.height:
for x in _board.width:
_paint_cell(Vector2i(x, y), hero)
_check_fog_completion()
_refresh_hover_after_paint()
@@ -296,14 +287,13 @@ func _badge_glyph(category: String) -> String:
return BADGE_PLACEHOLDER
# ---- 四格态渲染(灰度截图可辨:完整覆尘/碎裂裸土/雾纹带/已开雾 tint) ----
# ---- 格态渲染(灰度截图可辨:完整覆尘 / 碎裂裸土 / 已开数字) ----
func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
var btn: Button = _cells.get(c)
if btn == null:
return
var revealed: bool = _minesweeper.is_revealed(c)
var fog: bool = _board.variant_ids(c).size() > 0
var flag: bool = _minesweeper.has_flag(c)
var is_hero: bool = c == hero
var text := ""
@@ -315,8 +305,6 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
bg = COL_FLOOR
text = "雷"
digit_col = Color8(150, 44, 36)
elif fog:
bg = COL_FLOOR_FOG # 已开雾格 = 淡雾 tint 残留 + 无字(OQ8 裁决——与已开 0 格可判定区分)
else:
bg = COL_FLOOR
var d: int = rec.get("digit", 0)
@@ -327,10 +315,6 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
bg = COL_BRICK
text = "旗"
digit_col = COL_FLAG
elif fog:
bg = COL_BRICK_FOG
text = "雾"
digit_col = Color8(238, 232, 250)
else:
bg = COL_BRICK
var sb := StyleBoxFlat.new()
@@ -395,44 +379,6 @@ func _reset_breath() -> void:
sb.border_color = sb.bg_color.darkened(0.15)
# ---- 雾区全开一次性完成信号(视觉合并域 = 连通分量;实例独立、呈现合并——EC11) ----
func _compute_fog_parts() -> void:
var seen := {}
for y in _board.height:
for x in _board.width:
var c := Vector2i(x, y)
if seen.has(c) or _board.variant_ids(c).is_empty():
continue
var comp: Array = []
var queue: Array = [c]
seen[c] = true
while not queue.is_empty():
var cur: Vector2i = queue.pop_front()
comp.append(cur)
for n in _board.neighbors8(cur):
if not seen.has(n) and not _board.variant_ids(n).is_empty():
seen[n] = true
queue.append(n)
comp.sort_custom(func(a, b): return a.y * 1000 + a.x < b.y * 1000 + b.x)
_fog_parts.append(comp)
func _check_fog_completion() -> void:
for comp in _fog_parts:
var key: Vector2i = comp[0]
if _fog_done.has(key):
continue
var all_open := true
for c in comp:
if not _minesweeper.is_revealed(c):
all_open = false
break
if all_open:
_fog_done[key] = true
fog_region_completed.emit(comp.size())
# ---- 子标记层(不参与命中测试) ----
func _build_markers(btn: Button) -> Dictionary:
@@ -17,7 +17,6 @@ signal refresh_confirmed
signal legend_toggled
signal hover_forbidden(cell: Vector2i)
signal chord_attempted(cell: Vector2i)
signal fog_region_completed(region_size: int)
func _init() -> void:
@@ -43,7 +42,6 @@ func _init() -> void:
board_view.action_requested.connect(func(c: Vector2i, kind: String): action_requested.emit(c, kind))
board_view.hover_forbidden.connect(func(c: Vector2i): hover_forbidden.emit(c))
board_view.chord_attempted.connect(func(c: Vector2i): chord_attempted.emit(c))
board_view.fog_region_completed.connect(func(n: int): fog_region_completed.emit(n))
ap_bar.stop_pressed.connect(func(): stop_pressed.emit())
ap_bar.refresh_confirmed.connect(func(): refresh_confirmed.emit())
ap_bar.legend_toggled.connect(func(): legend_toggled.emit())
@@ -1,6 +1,6 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 实现 GDD:design/ux/battle.md(D 区图例按钮 + 模态 overlay——格态/变异/雾语义图例,
# 「迷雾 = 无雷盲区」条目恒在〔地形变异 D1×②〕;状态槽空态说明成文载入〔增益状态 D1×①〕)。
# 实现 GDD:design/ux/battle.md(D 区图例按钮 + 模态 overlay——格态语义图例;
# 状态槽空态说明成文载入〔增益状态 D1×①〕)。
# Date: 2026-09-12
# pattern 6 模态 overlay 层级纪律:打开时由组合根隐藏上方遮挡层 + 锁棋盘输入;
# 本类自持开关(toggle),探针断言"打开后自身 visible"——遮挡物断言归场景探针。
@@ -12,11 +12,9 @@ signal closed
const ENTRIES := [
["#C9AF83", "完整覆尘地砖", "未开格——左键挖开(1AP,须相邻)· 右键插旗(1AP)"],
["#B39A78", "碎裂裸土", "已开 0 格——可通行,左键移动(免费)"],
["#9691C4", "雾纹带(色相+\"雾\"字)", "未开雾格——数字被迷雾遮蔽,雾=无雷盲区"],
["#D8D0EA", "淡雾残留(无字)", "已开雾格——与已开 0 格同规则,雾不传播连锁"],
["#C63D22", "旗", "插旗标记——轮末自动判定标中;雾内插旗纯损"],
["#C63D22", "旗", "插旗标记——轮末自动判定标中"],
["#2E6E8E", "小人", "探险者——8 向相邻未开格常驻微光 = 可挖域"],
["#F0A828", "金徽章", "已开格奖励身份 icon(旗之下、纹带之上,不进数字区)"],
["#F0A828", "金徽章", "已开格奖励身份 icon(旗之下、数值之上,不进数字区)"],
["#33261B", "状态槽空态", "无状态效果时槽位隐藏,数据框上移补位——非异常"],
]
@@ -40,9 +38,7 @@ func _init() -> void:
box.custom_minimum_size = Vector2(520, 0)
box.add_theme_constant_override("separation", 6)
_panel.add_child(box)
box.add_child(UiKit.label("图例 · 格态与变异", 20, true))
# 「迷雾 = 无雷盲区」条目恒在(地形变异 图例条目恒在义务)
box.add_child(UiKit.label("迷雾 = 无雷盲区:雾格不藏雷,不需要赌命开雾。", 14, true))
box.add_child(UiKit.label("图例 · 格态", 20, true))
box.add_child(HSeparator.new())
for e in ENTRIES:
var row := HBoxContainer.new()
@@ -1,5 +1,5 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 实现 GDD:design/ux/battle.md(D 区 toast 槽——非模态提示:拒绝原因双源/voided/雾首遇/chord/
# 实现 GDD:design/ux/battle.md(D 区 toast 槽——非模态提示:拒绝原因双源/voided/chord/
# 零合法板;自动消退 2.5s;pattern 1/10 呈现通道)。
# Date: 2026-09-12
# 高内聚边界:纯呈现——组合根决定何时弹什么文案;本类只管槽位堆叠与消退动画。
@@ -28,8 +28,8 @@ func _initialize() -> void:
settlement.begin_battle({"level_index": level, "node_depth": 0, "hero_hp": db.hero.max_hp})
print("=== 棋盘摘要 L%d ===" % level)
var bd = settlement.minesweeper.current_board()
print("尺寸 %dx%d · 雷 %d · 安全格 %d · 雾格 %d · R₀ %d" % [bd.width, bd.height, bd.mines.size(), bd.safe_cells().size(), bd.fog_cells().size(), bd.metrics.r0_count])
print("指标: inferability=%.3f core=%.3f attempts=%s accept=%s" % [bd.metrics.inferability, bd.metrics.inferability_core, bd.metrics.attempts_used, bd.metrics.accept_mode])
print("尺寸 %dx%d · 雷 %d · 安全格 %d · R₀ %d" % [bd.width, bd.height, bd.mines.size(), bd.safe_cells().size(), bd.metrics.r0_count])
print("指标: inferability=%.3f attempts=%s accept=%s" % [bd.metrics.inferability, bd.metrics.attempts_used, bd.metrics.accept_mode])
var rounds := 0
while settlement.battle_result == "" and rounds < 40:
rounds += 1
@@ -7,22 +7,20 @@ func _initialize() -> void:
var db = load("res://core/config_db.gd").new()
db.load_tables("res://data/tables", true)
var board_generator = load("res://core/board_generator.gd").new()
print("=== 密度实验(L1 10×10 target0.95 / L2 12×12 雾 target0.92,各 5 种子)===")
print("d\t|M|/N\tR₀\tR₀/|S|\t残余挖掘\tinf\tcore\tatt")
print("=== 密度实验(L1 10×10 target0.88 / L2 12×12 target0.90,各 8 种子)===")
print("d\t|M|/N\tR₀\tR₀/|S|\t残余挖掘\tinf\tatt")
for d in [0.22, 0.25, 0.28, 0.30, 0.33]:
for cfg in [[10, 10, 0, 0.88, 50], [12, 12, 1, 0.90, 60]]:
for cfg in [[10, 10, 0.88, 50], [12, 12, 0.90, 60]]:
var w: int = cfg[0]
var h: int = cfg[1]
var fc: int = cfg[2]
var t: float = cfg[3]
var ms: int = cfg[4]
var agg := {"m": 0, "r0": 0, "rem": 0, "inf": 0.0, "core": 0.0, "att": 0, "n": 0}
var t: float = cfg[2]
var ms: int = cfg[3]
var agg := {"m": 0, "r0": 0, "rem": 0, "inf": 0.0, "att": 0, "n": 0}
for s in range(8):
var row := {
"level_index": 1, "node_depth": 0, "board_width": w, "board_height": h,
"mine_density": d, "inferability_target": t, "solver_rules": "single_subset",
"min_safe_count": ms, "max_gen_attempts": 16,
"fog_region_count": fc, "fog_target_ratio": 0.10 if fc > 0 else 0.0,
"board_biomes": ["dungeon"],
}
_patch_row(row, d)
@@ -36,13 +34,12 @@ func _initialize() -> void:
agg.r0 += int(b.metrics.r0_count)
agg.rem += b.safe_cells().size() - int(b.metrics.r0_count)
agg.inf += float(b.metrics.inferability)
agg.core += float(b.metrics.inferability_core)
agg.att += int(b.metrics.attempts_used)
agg.n += 1
if agg.n > 0:
var n := float(agg.n)
var n_placed: int = int(round(d * float(w * h - 9)))
print("%.2f %dx%d f%d\t%d/%d\t%d\t%.2f\t%d\t%.3f\t%.3f\t%.1f" % [d, w, h, fc, int(agg.m / n + 0.5), n_placed, int(agg.r0 / n + 0.5), agg.r0 / n / (w * h - int(agg.m / n + 0.5)), int(agg.rem / n + 0.5), agg.inf / n, agg.core / n, agg.att / n])
print("%.2f %dx%d\t%d/%d\t%d\t%.2f\t%d\t%.3f\t%.1f" % [d, w, h, int(agg.m / n + 0.5), n_placed, int(agg.r0 / n + 0.5), agg.r0 / n / (w * h - int(agg.m / n + 0.5)), int(agg.rem / n + 0.5), agg.inf / n, agg.att / n])
quit(0)
# --- 简易 db 替身:只供给本实验的行 ---
@@ -56,17 +53,13 @@ func _params_from(row: Dictionary) -> Dictionary:
"L": 1, "D": 0, "w": int(row.board_width), "h": int(row.board_height),
"density": float(row.mine_density), "target": float(row.inferability_target),
"solver_rules": "single_subset", "min_safe": int(row.min_safe_count),
"max_attempts": int(row.max_gen_attempts), "fog_count": int(row.fog_region_count),
"fog_ratio": float(row.fog_target_ratio), "biomes": ["dungeon"],
"max_attempts": int(row.max_gen_attempts), "biomes": ["dungeon"],
}
class FakeDB:
var row: Dictionary
func resolve_board_gen(_L: int, _D: int) -> Dictionary:
return row
func variant_pool(_L: int, _b: Array) -> Array:
return [{"variant_id": "fog", "unlock_level": 1, "scope": "region", "info_mask": "hide_numbers", "cell_constraint": "safe_nonzero", "biome_tags": [], "counter_item_ids": []}]
var action_economy := {"dig_ap_cost": 1}
func status_effect_row(_id: String) -> Dictionary:
return {}
@@ -42,7 +42,7 @@ func _initialize() -> void:
var reward_economy = reward_economy_factory.new(db, rng)
var list: Array = []
for c in eligible:
list.append({"cell": c, "fog": board.variant_ids(c).size() > 0})
list.append({"cell": c})
if not reward_economy.roll_board(list):
continue
var n_active := 0
@@ -63,8 +63,9 @@ func _run_core_smoke(db, rng) -> void:
_rng = rng
_smoke_wall_zero_guard()
_smoke_board_generator()
_smoke_full_battle(1, 0, "L1 · grub(无雾)")
_smoke_full_battle(2, 0, "L2 · ambusher(雾图)")
_smoke_variation_framework()
_smoke_full_battle(1, 0, "L1 · grub")
_smoke_full_battle(2, 0, "L2 · ambusher")
_smoke_level()
_smoke_flag_ap_exhausted()
_smoke_supply()
@@ -87,10 +88,10 @@ func _smoke_supply() -> void:
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(20260911)
var reward_economy_factory = load("res://core/reward_economy.gd")
# 合成资格清单:6 格含 1 雾格(fog=true)
# 合成资格清单:6 格
var eligible: Array = []
for i in range(6):
eligible.append({"cell": Vector2i(i, 0), "fog": i == 0})
eligible.append({"cell": Vector2i(i, 0)})
# ① 首件保证 + 每板恰 1 注入 + 注入占比(j=0 恒注入;种子固定 → 分布确定,宽容差断言)
var reward_economy = reward_economy_factory.new(_db, rng)
var ok_roll: bool = reward_economy.roll_board(eligible)
@@ -260,13 +261,13 @@ func _smoke_board_generator() -> void:
check(board_generator.legality(b1, row, _db), "L%d legality 全过" % L)
var inf := float(b1.metrics.get("inferability", 0.0))
var mode := String(b1.metrics.get("accept_mode", ""))
print(" L%d 实测:inferability=%.3f / inferability_core=%.3f / attempts_used=%d / accept_mode=%s / target=%.2f" % [
L, inf, float(b1.metrics.get("inferability_core", 0.0)), int(b1.metrics.get("attempts_used", -1)), mode, float(row.inferability_target)])
print(" L%d 实测:inferability=%.3f / attempts_used=%d / accept_mode=%s / target=%.2f" % [
L, inf, int(b1.metrics.get("attempts_used", -1)), mode, float(row.inferability_target)])
# 出图验收(棋盘生成 登记口径):first_pass 必须 ≥ target;accept_best(重试耗尽接受最优)允许低于 target
# 但必须打印实测值(正常出口);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.metrics.has("inferability_core"), "L%d 指标集输出 inferability_core(单一判据下仅作观测,不参与门槛)" % L)
check(b1.variants.is_empty(), "L%d 零变体格(地形变异 清单为空 → 生成期零布置)" % 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)])
@@ -288,6 +289,15 @@ func _mk_stream(stream_name: String, scope: int) -> RandomNumberGenerator:
return rg
# ---- 地形变异 框架面(P0 空清单合法 + 候选池门控通道在位——框架惰性非废弃)----
func _smoke_variation_framework() -> void:
print("--- 地形变异 框架面(空清单合法 + 候选池门控通道)---")
check(_db.variation.rows.is_empty(), "变异清单为空(P0 合法态——零变体格)")
check(_db.variant_pool(1, ["dungeon"]).is_empty(), "候选池为空——棋盘生成静默产出无变异图(不报错、不警告)")
check(_db.variation.row_by_id("nonexistent").is_empty(), "清单外 variant_id 返回空(读取通道在位)")
func _board_dump(b) -> String:
var parts: Array = [str(b.width), str(b.height), str(b.spawn_center)]
for y in b.height:
@@ -427,7 +437,7 @@ func _smoke_power_interval() -> void:
var reward_economy_factory = load("res://core/reward_economy.gd")
var eligible: Array = []
for i in range(6):
eligible.append({"cell": Vector2i(i, 0), "fog": false})
eligible.append({"cell": Vector2i(i, 0)})
var seen := {"heal_minor": {}, "knife_t1": {}, "heal_pack": {}, "knife_adv": {}, "clear_blast": {}} # rid → {roll 值集}
var in_domain := true
var fixed_ok := true
@@ -122,8 +122,12 @@ func _run_modal_probes_once() -> void:
_problems.append("图例打开时上方遮挡层未隐藏(缺陷⑤复发)")
if not main._input_locked():
_problems.append("图例打开时棋盘输入未锁定")
if not main.legend_overlay.ENTRIES[2][2].contains("无雷盲区"):
_problems.append("图例缺『迷雾 = 无雷盲区』恒在条目")
var legend_has_flag := false
for e in main.legend_overlay.ENTRIES:
if String(e[1]) == "旗":
legend_has_flag = true
if not legend_has_flag:
_problems.append("图例缺『旗』格态条目(格态图例基线)")
main.legend_overlay.close()
if main.legend_overlay.is_open() or main._input_locked():
_problems.append("图例关闭后未恢复输入/可见性")
@@ -97,24 +97,17 @@ func _ready() -> void:
if ic_off.text == "?" or ic_off.text == "":
problems.append("失衡图标键未映射(icon=%s)" % ic_off.text)
slots_row.set_effects([])
# ---- L2 雾图变体段(地形变异:雾首遇一次性提示 / 雾格呈现 / 去饱和接口可用性)----
# ---- L2 单场段(debug_session 单场原语 + 去饱和接口可用性;雾格机制已整体移除,不再断言变体呈现)----
# 2026-09-13 单场用户入口已删——探针直调 debug_session 原语(battle_created/ready 仍经组合根接线;
# _reset_run_scope 复现原 _start_single 的提示去重复位,保证 fog_first 断言前置态)
# _reset_run_scope 复现原 _start_single 的提示去重复位)
main._reset_run_scope()
main._debug.start_single(2)
await get_tree().process_frame
await get_tree().process_frame
if main.board_view.board() == null:
problems.append("L2 单场未启动(board=null)")
elif main.board_view.board().fog_cells().is_empty():
problems.append("L2 板无雾格(地形变异 变体缺失)")
elif not bool(main._hints_shown.get("fog_first", false)):
problems.append("雾首遇一次性提示未触发(fog_first)")
else:
if main.board_zone.toast._slot_box.get_child_count() < 1:
problems.append("雾首遇 toast 未呈现")
if main.board_view._fog_parts.is_empty():
problems.append("雾区连通分量未计算(合并呈现域)")
elif not main.board_view.board().variants.is_empty():
problems.append("L2 板出现变体格(地形变异 清单为空 → 零布置)")
main.board_view.set_desaturated(true)
main.board_view.set_desaturated(false)
if problems.is_empty():