fix(slice): 战斗阶段七项修复 + 顺手修七项 + 文档口径收编(含全仓语义命名清理)

代码修复(垂直切片,冒烟 193/193 · 三探针全绿):
- 遭遇流盐值修正(旧值超 int64 被截断,与 registry 黄金向量不符)+ 黄金向量断言入冒烟
- 完美排雷第二份按身份类型真实兑付;零兑付不发高光行;失败记"奖励未生效"行
- 轮内刷新图 provenance 守卫:刷新图清图不发清图奖励
- 可推理率求解器禁读雷位真值(no-peek)——L1/L2 实测 0.953/0.951、一次生成即达标
- 清图分类改传真实旗集(空旗集捷径会误判批次优先级)
- 达标判定回归单一判据(可推理率 ≥ 及格线)
- 等待期(Waiting)刷新通道整体拆除——非清图轮恒同图

顺手修:清图轮剩余 AP 当场清零 + 耗尽轮断言;degraded_product 降级标记;产物雷数 ≤N 断言;
明细流水"无来源即炸"及两层豁免成文;主角零值伤害行;窗口段三锚行(13→15 类行协议);
图标键 fail-fast + 补齐暴走/失衡两键。

文档:可选类注入递减基准改"按实际获取件数"、首件保证改"必注入"口径(+未挖到不补发待办);
教学位掷骰口径自洽;标错反噬三池收编(状态表四行→六行);行协议 13→15 与豁免清单回写
《战斗结算》与 registry,下游引用同步收敛。

另含:全仓旧代号→语义命名清理(注释/标识符,81 文件);CLAUDE.md 登记 Godot 路径与验证命令。
This commit is contained in:
DKChan committed 2026-09-15 20:46:21 +08:00
1 parent 9cca04c9af
commit cb548776e0
89 files changed
+2139 -2230

No files matched your search

@@ -54,7 +54,7 @@ Godot_v4.7.2-stable_win64_console.exe --headless --path . --script res://smoke/s
- **v1 在范围内**:D2(10 表 + ConfigDB + RngService)· A1(single_subset 求解器 + 双判据验收)· A2 · A3(AP8/RAP)· A4(迷雾,L2)· B1(七段结算 + 13 类行协议 + 高光双落)· B2(grub/ambusher/伏击池 cap4)· B3(四行状态容器)· B4(install 预分配/F5 保底/道具窗口)· D1-lite 程序化呈现
- **v2 追加**:LevelTable(线性节点链数据表)· C1-lite 关卡推进器 `level_runner.gd`(原 c1_level_runner,2026-09-11 领域命名批更名)(关卡域生命周期:共享 B4 关内延续 + HP 跨战斗携带 + 奖励/恢复节点兑付 + 关卡末清库)· Boss 遭遇行(Dmax=4)· 节点奖励 API(B4 `grant_node_active`)· **测试用角色数值调整板**(atk%/hp% 覆盖 + 批量模拟,模拟天赋树——正式侧归 C4)
- **裁剪不变**:A5 / C1 路径分支选择(用户拍板线性)· C2–C4 / B5 战斗动画 / D3–D6 / A1 黄金向量与 F4 批量工具 / gdUnit / 标错惩罚(A2 OQ1)
- **裁剪不变**:A5 / C1 路径分支选择(用户拍板线性)· C2–C4 / B5 战斗动画 / D3–D6 / A1 黄金向量与 F4 批量工具 / gdUnit · 注:标错惩罚(原 A2 OQ1)已于 2026-09-12 批上线(标空旗三选一确定性反噬:暴走 / 伏击 +2 层 / 失衡,坐标×板序号派生、不走随机流),本批(2026-09-15)已收编进现行文档,不再属裁剪项
## 2026-09-11 可选类供给控制器(用户拍板批——修改记录)
@@ -95,6 +95,10 @@ Godot_v4.7.2-stable_win64_console.exe --headless --path . --script res://smoke/s
**正式侧义务(GDD/registry 修订清单)**:A3 GDD Rule 11(受理相位 Waiting → Active∪Waiting + 相位机表 + 时间税语义注记)· A2 GDD install 四不变式轮内场景 · B1 GDD 编排专章(轮内换图 + facts 快照协议)· B4 GDD E5/E6(query_full_snapshot/grant 快照直通)· game-concept 轮次流程 + "战斗:扫雷图 = 1:M"措辞(一轮跨多图新形态)· registry `action_economy`(refresh_max_per_round)· 数值连锁重跑(清图支配性条款⑤/获取期望——模拟不覆盖真人刷图行为,需 playtest 实测)。
**2026-09-15 补记(用户拍板)**:**Waiting(等待结算)相位刷新通道整体移除**——呈现层入口此前已按 OQ12 退役(2026-09-12 战斗屏批),本批拆除核心层残留:账本侧 Waiting 受理入口 `request_refresh()` 删除、编排侧 `can_request_refresh` 不再放行 Waiting、`player_request_refresh` 等待期登记意图分支删除、出口分发删除 `_refresh_used_this_waiting → new_board` 换图分支(非清图轮恒同图)、字段与诊断快照键同步移除。文档侧无需修订——[行动经济](../../design/gdd/systems/排雷循环/行动经济.md)只写挖掘期刷新、[战斗结算](../../design/gdd/systems/战斗循环/战斗结算.md)明文"非清图轮恒同图",拆完代码即双双收敛。冒烟断言改为"等待期刷新请求被拒且状态不变"。
**同批其它改动(口径读法)**:明细行协议 **13 类 → 15 类**(新增窗口打开锚行 `window_open`、窗口跳过锚行 `window_skip`,服务"窗口段恒在案"验收;单点定义见[战斗结算](../../design/gdd/systems/战斗循环/战斗结算.md));状态表 **四行 → 六行**(暴走 / 失衡两行随标错反噬批收编);达标判定回归**单一判据**(可推理率 ≥ 及格线);求解器禁读雷位真值。本文正文的 v1/v2 范围登记与各历史批条目保留当时陈述,计数以现行文档与代码为准。
## 2026-09-11 关卡扩展批 · 切片级偏差登记(实现期决策,正式化时须回 GDD/ADR)
1. **C1 线性推进器 provisional**:节点链来自 LevelTable(结构位 = 未来 C1 的数据承载面);路径分支/奖励点分布定稿权归 C1/C3。节点类型 closed enum {battle, reward, rest, boss};末节点必须 Boss(LevelTable schema 校验)。
@@ -1,13 +1,13 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
# Date: 2026-09-10
# A3 行动经济:被动账本(AP 硬重置 / 统一价目 / 三路径结束仲裁 cleared > stopped > ap_exhausted /
# 行动经济:被动账本(AP 硬重置 / 统一价目 / 三路径结束仲裁 cleared > stopped > ap_exhausted /
# RAP 池战斗域 / 双发射源之 stopped 直发)。相位域 = {Idle, Active, Waiting}。
# 实现 GDD:design/archive/gdd-2026-09-14/a3-action-economy.md(系统 ID A3——registry 追溯锚,命名规范见 CLAUDE.md)
# 实现 GDD:design/archive/gdd-2026-09-14/a3-action-economy.md(系统 ID A3·行动经济——registry 追溯锚,命名规范见 CLAUDE.md)
class_name ActionEconomy
extends RefCounted
signal round_end_stopped(payload: Dictionary) # 双发射源之停手路径(A3 直发;payload 与 A2 棋盘路径同构)
signal round_end_stopped(payload: Dictionary) # 双发射源之停手路径(行动经济 直发;payload 与 排雷玩法 棋盘路径同构)
const PHASE_IDLE := "Idle"
const PHASE_ACTIVE := "Active"
@@ -25,7 +25,7 @@ var ap: int:
var _rap := 0
var rap: int:
get: return _rap
var _last_round_end: Dictionary = {} # {path_tag, continuation_hint}——A2 组包 round_end 的合法数据源
var _last_round_end: Dictionary = {} # {path_tag, continuation_hint}——排雷玩法 组包 round_end 的合法数据源
var last_round_end: Dictionary:
get: return _last_round_end
var _settlement_consumed := false # AC-14 门:notify_settlement_done 消费后才受理 round_start
@@ -53,7 +53,7 @@ func _cost(kind: String) -> int:
return 0
# 通用扣减(A2 用于旗动作;挖握走 request_dig——cleared 仲裁需要 clears_map 参数)
# 通用扣减(排雷玩法 用于旗动作;挖握走 request_dig——cleared 仲裁需要 clears_map 参数)
func try_spend(action_kind: String) -> bool:
if phase != PHASE_ACTIVE:
_reject("phase")
@@ -61,7 +61,7 @@ func try_spend(action_kind: String) -> bool:
return _spend_and_arbitrate(action_kind, false)
# A2 挖掘专用:chain_size = 连锁附加格数(不含首格);clears_map = 本次动作后全图安全格开完
# 排雷玩法 挖掘专用:chain_size = 连锁附加格数(不含首格);clears_map = 本次动作后全图安全格开完
func request_dig(chain_size: int, clears_map: bool) -> bool:
if phase != PHASE_ACTIVE:
_reject("phase")
@@ -76,7 +76,8 @@ func _spend_and_arbitrate(action_kind: String, clears_map: bool) -> bool:
return false
_ap -= cost
if clears_map:
# cleared 胜出(同帧仲裁 cleared > ap_exhausted;剩余 AP 作废不入 RAP——EC7/EC10)
# cleared 胜出(同帧仲裁 cleared > ap_exhausted);剩余 AP 当场清零作废、不进 RAP(EC7/EC10)
_ap = 0
_finish_round("cleared", "new_board")
elif _ap == 0:
# AP 归 0 瞬间轮次结束——不存在 0-AP 操作窗口
@@ -84,7 +85,7 @@ func _spend_and_arbitrate(action_kind: String, clears_map: bool) -> bool:
return true
# 停手:剩余 AP 1:1 转存 RAP;A3 直发 stopped 路径 round_end(双发射源)
# 停手:剩余 AP 1:1 转存 RAP;行动经济 直发 stopped 路径 round_end(双发射源)
func request_stop() -> bool:
if phase != PHASE_ACTIVE:
_reject("phase")
@@ -96,20 +97,9 @@ func request_stop() -> bool:
return true
# 玩家发起的刷新(B1 在 Waiting 编排期代理调用;禁止 RAP 满自动刷)
func request_refresh() -> bool:
if phase != PHASE_WAITING:
_reject("phase")
return false
if _rap < db.action_economy.rap_refresh_cost:
return false # EC13 资源不足拒绝——合法相位内资源拒绝,非编排错误
_rap -= db.action_economy.rap_refresh_cost
return true
# 轮内刷新(2026-09-12 方案 A 用户拍板):Active 相位受理——仅扣 RAP,相位与 AP 均不动。
# 刷新不打断轮次、不触发结算(敌方时间税语义变更已登记 GDD 修订义务);换图编排归 B1
# (retire + install 立即执行,玩家同轮继续挖新图)。决策主体 = 玩家(经 D1 发起)纪律不变。
# 轮内刷新(2026-09-12 方案 A 用户拍板;等待期通道 2026-09-15 用户拍板整体移除):唯一入口 =
# Active 相位——仅扣 RAP,相位与 AP 均不动。刷新不打断轮次、不触发结算;换图编排归 战斗结算
# (retire + install 立即执行,玩家同轮继续挖新图)。决策主体 = 玩家(经 HUD框架发起)纪律不变。
func request_refresh_active() -> bool:
if phase != PHASE_ACTIVE:
_reject("phase")
@@ -120,7 +110,7 @@ func request_refresh_active() -> bool:
return true
# B1 回发:结算完成(仅记录编排意图;phase 停留 Waiting——AC-14 门不被绕过)
# 战斗结算 回发:结算完成(仅记录编排意图;phase 停留 Waiting——AC-14 门不被绕过)
func notify_settlement_done(next_round_mode: String) -> bool:
if phase != PHASE_WAITING:
_reject("phase")
@@ -149,15 +139,19 @@ func notify_combat_end() -> bool:
_reject("phase")
return false
_phase = PHASE_IDLE
_rap = 0 # RAP 战斗域清零(用户拍板;D1 可见反馈义务)
_rap = 0 # RAP 战斗域清零(用户拍板;HUD框架可见反馈义务)
_ap = 0
return true
func _finish_round(path_tag: String, continuation_hint: String) -> void:
# 实现层断言兜底(行动经济 轮次结束仲裁表):耗尽轮剩余恒为 0;清图轮剩余已当场清零(不进储备池)。
# 两断言守在唯一仲裁出口,未来若在扣费与仲裁之间挪动 AP 写入在此红灯。
assert(path_tag != "ap_exhausted" or _ap == 0, "耗尽轮剩余行动点恒为 0(实现层断言兜底)")
assert(path_tag != "cleared" or _ap == 0, "清图轮剩余行动点当场清零(不进储备池)")
_last_round_end = {"path_tag": path_tag, "continuation_hint": continuation_hint}
_phase = PHASE_WAITING
func _reject(source: String) -> void:
push_warning("[A3] 动作被拒(%s 相位守卫/资源)" % source if source == "phase" else "[A3] 资源不足拒绝")
push_warning("[行动经济] 动作被拒(%s 相位守卫/资源)" % source if source == "phase" else "[行动经济] 资源不足拒绝")
@@ -1,12 +1,12 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
# Date: 2026-09-10
# B1 战斗结算编排中枢:轮次生命周期唯一 owner · 段序骨架(授予→窗口→增益/减益→道具→战斗→衰减→判定)·
# 13 类行协议可回放步骤流(零随机零 await)· 高光双落(完美排雷 ×2 / 标中旗 highlight)· 伏击预截流 ·
# 战斗结算编排中枢:轮次生命周期唯一 owner · 段序骨架(授予→窗口→增益/减益→道具→战斗→衰减→判定)·
# 15 类行协议可回放步骤流(零随机零 await;2026-09-15 窗口锚行批 13→15)· 高光双落(完美排雷 ×2 / 标中旗 highlight)· 伏击预截流 ·
# Waiting 相位结算触发(不依赖事件到达序)。
# 切片登记:① 事件总线载体 = 直连信号(实现期 ADR 项,provisional)② 多次刷新 EC15 简化为每 Waiting 至多
# 一次(UI 禁二次发起;多刷契约义务不触)③ 效果行协议 = 授予段逐 grant 行 + 增益/减益段续存实例组合行。
# 实现 GDD:design/archive/gdd-2026-09-14/b1-settlement.md(系统 ID B1——registry 追溯锚,命名规范见 CLAUDE.md)
# 切片登记:① 事件总线载体 = 直连信号(实现期 ADR 项,provisional)② Waiting 相位刷新通道 2026-09-15 用户拍板整体
# 拆除(仅 Active 挖掘期受理;旧"每 Waiting 至多一次"简化登记随之作废)③ 效果行协议 = 授予段逐 grant 行 + 增益/减益段续存实例组合行。
# 实现 GDD:design/archive/gdd-2026-09-14/b1-settlement.md(系统 ID B1·战斗结算——registry 追溯锚,命名规范见 CLAUDE.md)
class_name BattleOrchestrator
extends RefCounted
@@ -17,18 +17,29 @@ signal round_started(round_index: int)
signal board_changed()
signal settlement_snapshot(snap: Dictionary) # 每结算末尾的全量状态快照(playtest_logger 消费——JSONL 数值分析)
# 明细流水行型协议(战斗结算 Core Rule 14;15 类——2026-09-15 窗口锚行批由 13 类扩为 15 类,
# 清单改动须同步 battle_log_view 的 _format_row 与本文档头注):
# 效果行 effect · 伤害行 damage · 治疗行 heal · 道具行 item · 溢出未生效行 overflow ·
# 奖励未生效行 reward_ineffective · 已消耗未生效行 consumed_ineffective · 使用被拒行 use_rejected ·
# 标旗未中行 flag_miss · 蓄力行 charge · 衰减行 decay · 判定行 verdict · 高光行 highlight ·
# 窗口打开锚行 window_open · 窗口跳过锚行 window_skip(后两类 = 2026-09-15 补的窗口段锚行)
# 行来源断言豁免集合(行型层)——判定段 / 衰减段 / 窗口锚行本就无棋盘来源,勿一刀切误炸
const SOURCE_EXEMPT_TYPES := ["decay", "verdict", "window_open", "window_skip"]
# 行来源断言豁免集合(来源类别层)——事实来源是单位 / 清图 / 库存件而非棋盘格
const SOURCE_EXEMPT_CLASSES := ["hero_attack", "enemy_action", "clear_reward", "item"]
var db
var rng
# E1 封装收口(2026-09-13):协作对象 a1/b2/b3/b4 私有化——跨层读取一律经 presentation_snapshot()/
# diagnostic_snapshot() 成文出口(呈现层链式穿透 b1._b2.hero_unit() 的根因收口)。
# a2/a3 保持公有 = 架构文档 API Boundaries 的 D1 消费面(意图提交 settle_batch + compute_move_path
# E1 封装收口(2026-09-13):协作对象 board_generator/enemy_system/status_effects/reward_economy 私有化——跨层读取一律经 presentation_snapshot()/
# diagnostic_snapshot() 成文出口(呈现层链式穿透 settlement._enemy_system.hero_unit() 的根因收口)。
# minesweeper/action_economy 保持公有 = 架构文档 API Boundaries 的 HUD框架消费面(意图提交 settle_batch + compute_move_path
# 只读;ap/rap/相位读取)——非本文档登记的契约面,勿增新用途。
var _a1: BoardGenerator
var a2: MinesweeperRules
var a3: ActionEconomy
var _b2: UnitContainer
var _b3: StatusContainer
var _b4: RewardEconomy
var _board_generator: BoardGenerator
var minesweeper: MinesweeperRules
var action_economy: ActionEconomy
var _enemy_system: UnitContainer
var _status_effects: StatusContainer
var _reward_economy: RewardEconomy
var encounter: Dictionary = {}
var round_index := 0
@@ -40,9 +51,9 @@ var _facts_path := ""
var _settling := false
var _awaiting_window := false
var _window_items: Array = []
var _refresh_used_this_waiting := false
var _refresh_used_this_round := 0 # 每轮刷新计数(Active+Waiting 合计,兜底上限 = ActionEconomyTable.refresh_max_per_round)
var _refresh_used_this_round := 0 # 每轮刷新计数(Active 相位合计,兜底上限 = ActionEconomyTable.refresh_max_per_round)
var _board_seq := 0 # 板序号(本战斗第几张板——快照归因用)
var _board_from_midround_refresh := false # 装板 provenance:轮内刷新换来的图 = true(清图奖励守卫——刷新图不发清图奖励)
var _flag_consumed := {} # 兑付资格以雷格实例为键(板级;换图重置)
var _flag_miss_seen := {} # 标旗未中行去重(每雷格实例一次)
var _perfect_hit_true := 0 # 板级完美排雷计数(install 重置)
@@ -54,38 +65,38 @@ var _charge_dmg_a := 0 # 伤害A 充能:本轮开出的资
var _charge_dmg_b := 0 # 伤害B 充能:本轮首次标中雷计数 ×damage_b_per_flag(每雷实例一次)
func _init(db_ref, rng_ref, shared_b4 = null) -> void:
func _init(db_ref, rng_ref, shared_reward_economy = null) -> void:
db = db_ref
rng = rng_ref
_a1 = BoardGenerator.new()
a3 = ActionEconomy.new(db)
a2 = MinesweeperRules.new(db, a3)
_b2 = UnitContainer.new(db)
_b3 = StatusContainer.new(db)
# 库存关卡域(2026-09-11 关内延续):连续关卡模式注入共享 B4 容器,库存跨战斗延续
_b4 = shared_b4 if shared_b4 != null else RewardEconomy.new(db, rng)
a2.cell_revealed.connect(_on_cell_revealed)
a2.flag_judged.connect(_on_flag_judged)
a2.round_end_board.connect(_on_round_end)
a3.round_end_stopped.connect(_on_round_end)
_board_generator = BoardGenerator.new()
action_economy = ActionEconomy.new(db)
minesweeper = MinesweeperRules.new(db, action_economy)
_enemy_system = UnitContainer.new(db)
_status_effects = StatusContainer.new(db)
# 库存关卡域(2026-09-11 关内延续):连续关卡模式注入共享 奖励道具经济 容器,库存跨战斗延续
_reward_economy = shared_reward_economy if shared_reward_economy != null else RewardEconomy.new(db, rng)
minesweeper.cell_revealed.connect(_on_cell_revealed)
minesweeper.flag_judged.connect(_on_flag_judged)
minesweeper.round_end_board.connect(_on_round_end)
action_economy.round_end_stopped.connect(_on_round_end)
# ---- 战斗开始序列(编排专章①:门 → B3 → B2 → install+roll → A3)----
# ---- 战斗开始序列(编排专章①:门 → 增益状态 → 敌人系统 → install+roll → 行动经济)----
func begin_battle(enc: Dictionary) -> bool:
if round_index != 0:
push_warning("[B1] begin_battle 重复受理")
push_warning("[战斗结算] begin_battle 重复受理")
return false
encounter = enc
db.set_reload_gate(true)
if not _b3.notify_battle_start():
_fatal("B3 开域被拒(编排违例)")
if not _status_effects.notify_battle_start():
_fatal("增益状态 开域被拒(编排违例)")
return false
_b4.notify_battle_start() # 可选类供给控制器战斗域清零(2026-09-11 供给控制器批——首件保证按战重置)
# 遭遇骰(C3 GDD Core Rule 8——2026-09-12 用户拍板):行有 unit_count_max 时按 [unit_count, max] 均匀骰;
_reward_economy.notify_battle_start() # 可选类供给控制器战斗域清零(2026-09-11 供给控制器批——首件保证按战重置)
# 遭遇骰(难度曲线与Boss GDD Core Rule 8——2026-09-12 用户拍板):行有 unit_count_max 时按 [unit_count, max] 均匀骰;
# 2026-09-14 敌方攻击区间批扩展:行有 atk_min 时按 [atk_min, atk_max] 骰 spawn 期整战持久值
# (每拍抖动否决——数字可读性锚;中点 = 行值)。encounter 封闭流恰一 scope(数量骰先、atk 骰后);
# 挖掘决策域之外——P1 合规;B2 零随机纪律保持(收 override 即确定值)。
# 挖掘决策域之外——P1 合规;敌人系统 零随机纪律保持(收 override 即确定值)。
var erow_pre: Dictionary = db.resolve_enemy_rowset(int(enc.level_index), int(enc.node_depth))
var roll_lo: int = int(erow_pre.get("unit_count", 1))
var roll_hi: int = int(erow_pre.get("unit_count_max", roll_lo))
@@ -101,9 +112,9 @@ func begin_battle(enc: Dictionary) -> bool:
rolled_atk = int(erow_pre.atk_min) + estream.randi_range(0, int(erow_pre.atk_max) - int(erow_pre.atk_min))
# E4 收口:加成经 encounter 显式携带(hero_atk/hero_max_hp;缺省 = 表值——冒烟/工具直呼路径)
_hero_atk = int(enc.get("hero_atk", int(db.hero.atk)))
if not _b2.spawn(int(enc.level_index), int(enc.node_depth), int(enc.hero_hp), rolled,
if not _enemy_system.spawn(int(enc.level_index), int(enc.node_depth), int(enc.hero_hp), rolled,
_hero_atk, int(enc.get("hero_max_hp", -1)), rolled_atk):
_fatal("B2 spawn 被拒")
_fatal("敌人系统 spawn 被拒")
return false
var erow: Dictionary = db.resolve_enemy_rowset(int(enc.level_index), int(enc.node_depth))
_enemy_cap = int(erow.get("advantage_per_round_cap", 0))
@@ -111,8 +122,8 @@ func begin_battle(enc: Dictionary) -> bool:
if not _install_new_board():
return false
_reset_round_facts() # 首板 install 的初始展开事件不入轮 facts(reward_eligible 恒 false 的双保险)
if not a3.notify_round_start():
_fatal("A3 首轮 notify_round_start 被拒")
if not action_economy.notify_round_start():
_fatal("行动经济 首轮 notify_round_start 被拒")
return false
round_index = 1
round_started.emit(round_index)
@@ -121,14 +132,14 @@ func begin_battle(enc: Dictionary) -> bool:
func _install_new_board(during_round := false) -> bool:
var stream: RandomNumberGenerator = rng.open_stream("board_gen")
var board: BoardData = _a1.generate(int(encounter.level_index), int(encounter.node_depth), stream, db)
var board: BoardData = _board_generator.generate(int(encounter.level_index), int(encounter.node_depth), stream, db)
if board == null:
return false
var res: Dictionary = a2.install_board(board, during_round)
var res: Dictionary = minesweeper.install_board(board, during_round)
if not res.accepted:
_fatal("A2 install 拒绝(不变式违例): %s" % str(res))
_fatal("排雷玩法 install 拒绝(不变式违例): %s" % str(res))
return false
# B4 义务⑥:roll_board 恰一次(install 后、挖掘前);资格格 = S ∖ R₀
# 奖励道具经济 义务⑥:roll_board 恰一次(install 后、挖掘前);资格格 = S ∖ R₀
var r0 := {}
for c in board.compute_r0():
r0[c] = true
@@ -137,13 +148,14 @@ func _install_new_board(during_round := false) -> bool:
if r0.has(c):
continue
eligible.append({"cell": c, "fog": board.variant_ids(c).size() > 0})
if not _b4.roll_board(eligible):
_fatal("B4 roll_board 被拒")
if not _reward_economy.roll_board(eligible):
_fatal("奖励道具经济 roll_board 被拒")
return false
_flag_consumed.clear() # 换图新雷 = 新实例,资格自然重置(A3 Rule 8)
_flag_consumed.clear() # 换图新雷 = 新实例,资格自然重置(行动经济 Rule 8)
_flag_miss_seen.clear()
_perfect_hit_true = 0 # 板级完美计数随 install 重置
_perfect_hit_false = 0
_board_from_midround_refresh = during_round # 轮内刷新装板 = 刷新图(清图奖励不发);常规换图/开战装板 = false
_board_seq += 1
board_changed.emit()
return true
@@ -159,7 +171,7 @@ func _on_cell_revealed(evt: Dictionary) -> void:
if evt.kind == "safe" and bool(evt.get("reward_eligible", false)):
# 身份快照随缓冲捕获(批次隔离——轮内刷新后 mapping 已换批,授予必须用缓冲期身份,
# 防旧图已挖格在轮末错拿新图同坐标身份)
var snap = _b4.query_full_snapshot(evt.cell)
var snap = _reward_economy.query_full_snapshot(evt.cell)
if snap != null:
evt = evt.duplicate()
evt["reward_snapshot"] = snap
@@ -178,7 +190,7 @@ func _on_round_end(_payload: Dictionary) -> void:
func _maybe_settle() -> void:
if _settling or battle_result != "":
return
if a3.phase != a3.PHASE_WAITING:
if action_economy.phase != action_economy.PHASE_WAITING:
return
_settle()
@@ -196,7 +208,7 @@ func _settle() -> void:
elif evt.kind == "safe" and evt.reward_eligible:
_charge_dmg_a += 1 # 伤害A 充能:探索本身计费,与该格奖励是否兑付成功无关(初始展开格 reward_eligible=false 不计)
_grant_cell(evt.cell, ri, "safe_cell", evt.get("reward_snapshot"))
# reward_eligible=false(initial 通道)不入库——B1 义务②(A3 EC5 委派闭环)
# reward_eligible=false(initial 通道)不入库——战斗结算 义务②(行动经济 EC5 委派闭环)
for evt in _facts_flags:
_judge_flag(evt, ri)
if not mine_cells.is_empty():
@@ -206,22 +218,26 @@ func _settle() -> void:
# ② 道具使用窗口(授予段后、增益/减益段前——当轮拾取当轮可用)
_window_items = _assemble_window()
if not _window_items.is_empty():
# 窗口打开锚行(战斗结算 验收:"窗口段恒在案")——零棋盘来源,行型豁免
_emit_row("window_open", "", Vector2i(-1, -1), ri, "道具窗口(%d 件可选)" % _window_items.size(), _window_items.size(), {})
_awaiting_window = true
window_opened.emit(_window_items)
_settling = false
return # 窗口选择序列 = 结算回放输入事件
# 空库存不弹窗,但窗口段仍要在案("空库存记跳过")——锚行不可省
_emit_row("window_skip", "", Vector2i(-1, -1), ri, "道具窗口跳过(空库存)", 0, {})
_finish_settlement([])
func _assemble_window() -> Array:
# 窗口数据组装 = B4 query_inventory 投影 + B1 侧守卫预判 usable_now(B4 零跨系统读不破)
# 窗口数据组装 = 奖励道具经济 query_inventory 投影 + 战斗结算 侧守卫预判 usable_now(奖励道具经济 零跨系统读不破)
var out: Array = []
for inst in _b4.query_inventory():
for inst in _reward_economy.query_inventory():
if inst.status != "InStock":
continue
var usable := true
if inst.category == "heal_pack":
usable = int(_b2.hero_unit().hp) < int(_b2.hero_unit().max_hp) # 满血治疗包禁选
usable = int(_enemy_system.hero_unit().hp) < int(_enemy_system.hero_unit().max_hp) # 满血治疗包禁选
out.append({
"index": inst.index,
"item_id": inst.item_id,
@@ -237,18 +253,22 @@ func _assemble_window() -> Array:
func submit_window_selection(selection: Array) -> void:
if not _awaiting_window:
push_warning("[B1] 无窗口期的选择提交(编排违例)")
push_warning("[战斗结算] 无窗口期的选择提交(编排违例)")
return
_awaiting_window = false
_settling = true
if selection.size() > 1:
push_warning("[B1] 窗口至多选用一件(2026-09-11 用户拍板——避免结算复杂度)——超出部分忽略")
_finish_settlement(selection.slice(0, 1))
push_warning("[战斗结算] 窗口至多选用一件(2026-09-11 用户拍板——避免结算复杂度)——超出部分忽略")
var picked: Array = selection.slice(0, 1)
if picked.is_empty():
# 零选择跳过锚行(战斗结算 验收:"零选择记跳过")——窗口段在案的第二半
_emit_row("window_skip", "", Vector2i(-1, -1), round_index, "道具窗口跳过(未选用)", 0, {})
_finish_settlement(picked)
func _finish_settlement(selection: Array) -> void:
var ri := round_index
# ③ 增益/减益段(续存实例组合行;本结算新授予的已在授予段逐 grant 出行——B3 义务⑦)
# ③ 增益/减益段(续存实例组合行;本结算新授予的已在授予段逐 grant 出行——增益状态 义务⑦)
_emit_composition_effects("hero", ri)
_emit_composition_effects("enemy", ri)
# ④ 道具段(窗口选定序逐件生效;逐件动态守卫)
@@ -257,13 +277,13 @@ func _finish_settlement(selection: Array) -> void:
# ⑤ 战斗段(调用序钉死 query_attack → apply_damage → advance;hero hp=0 短路)
_combat_segment(ri)
# ⑥ 衰减段(恰一次恒执行含末轮——先兑付后衰减)
var k: int = _b3.decay_round()
var k: int = _status_effects.decay_round()
_emit_row("decay", "", Vector2i(-1, -1), ri, "状态衰减", k, {}, "")
# ⑦ 判定段(防御性优先级 hero 先判——双死结构不可达的纵深防御)
var verdict := "continue"
if int(_b2.hero_unit().get("hp", 0)) <= 0:
if int(_enemy_system.hero_unit().get("hp", 0)) <= 0:
verdict = "defeat"
elif _b2.query_alive("enemy").is_empty():
elif _enemy_system.query_alive("enemy").is_empty():
verdict = "victory"
_emit_settlement_snapshot(verdict) # 全量状态快照(despawn 前组装——终局含 result)
if verdict == "defeat":
@@ -283,55 +303,57 @@ func _finish_settlement(selection: Array) -> void:
# snap_override 非空 = 冻结快照授予(轮内刷新批——缓冲期身份,绕过现行 mapping/_granted)
func _grant_cell(cell: Vector2i, ri: int, source_class: String, snap_override = null) -> void:
var snap = snap_override if snap_override != null else _b4.grant(cell)
var snap = snap_override if snap_override != null else _reward_economy.grant(cell)
if not (snap is Dictionary):
return
if snap.category == "buff":
var ok: bool = _b3.apply_effect(snap.effect_id, {"source_class": source_class, "origin_cell": cell, "round_index": ri})
var ok: bool = _status_effects.apply_effect(snap.effect_id, {"source_class": source_class, "origin_cell": cell, "round_index": ri})
if ok:
var row: Dictionary = db.status_effect_row(snap.effect_id)
var pool := _pool_effective(snap.effect_id)
_emit_row("effect", source_class, cell, ri, String(row.display_name), int(row.magnitude), {"effect_id": snap.effect_id, "pool_value": pool})
else:
_emit_row("reward_ineffective", source_class, cell, ri, "奖励未生效(满层)", 0, {"batch_key": String(snap.effect_id)})
elif _resolve_instant(snap, source_class, cell, ri):
pass # 即时类:授予段当场兑付(不进库存、零决策——2026-09-11 两类奖励裁决)
elif _resolve_instant(snap, source_class, cell, ri) != "not_instant":
pass # 即时类:授予段当场兑付(兑付行/零兑付未生效行均由 _resolve_instant 自记——2026-09-11 两类奖励裁决)
else:
_b4.stock(snap, cell, ri)
_reward_economy.stock(snap, cell, ri)
_emit_row("item", source_class, cell, ri, "%s 入库" % snap.display_name, int(snap.power), {})
# 即时类当场兑付(2026-09-11 两类奖励裁决):instant_aoe = 全体存活敌各 power 点;
# instant_heal = 主角 +power(clamp,满血 → "奖励未生效"行)。返回 false = 非即时类(道具类走库存)
func _resolve_instant(snap: Dictionary, source_class: String, cell: Vector2i, ri: int, suffix: String = "", highlight: String = "") -> bool:
# instant_heal = 主角 +power(clamp,满血 → "奖励未生效"行)。
# 返回 "redeemed" = 实际有效兑付 / "zero" = 零兑付(奖励未生效行已记)/ "not_instant" = 非即时类(道具类走库存)。
# 零兑付必须可区分:完美排雷高光行只记实际有效兑付,首份即零兑付则高光行不发(战斗结算 授予段规则)。
func _resolve_instant(snap: Dictionary, source_class: String, cell: Vector2i, ri: int, suffix: String = "", highlight: String = "") -> String:
if snap.category == "instant_aoe":
var alive: Array = _b2.query_alive("enemy")
var alive: Array = _enemy_system.query_alive("enemy")
if alive.is_empty():
_emit_row("reward_ineffective", source_class, cell, ri, "%s 未生效(无存活敌人)%s" % [snap.display_name, suffix], 0, {"batch_key": "instant_aoe"}, highlight)
return true
return "zero"
for u in alive:
_b2.apply_damage(u, int(snap.power)) # 恒定表值,逐个;受击即死——死亡即不动手
_enemy_system.apply_damage(u, int(snap.power)) # 恒定表值,逐个;受击即死——死亡即不动手
_emit_row("damage", source_class, cell, ri, "%s → 全体敌人 ×%d%s" % [snap.display_name, alive.size(), suffix], int(snap.power), {"target": "all_enemies", "count": alive.size()}, highlight)
return true
return "redeemed"
if snap.category == "instant_heal":
if int(_b2.hero_unit().hp) >= int(_b2.hero_unit().max_hp):
if int(_enemy_system.hero_unit().hp) >= int(_enemy_system.hero_unit().max_hp):
_emit_row("reward_ineffective", source_class, cell, ri, "%s 未生效(满血)%s" % [snap.display_name, suffix], 0, {"batch_key": "instant_heal"}, highlight)
return true
var healed: int = _b2.heal(_b2.hero_unit(), int(snap.power)) # clamp 归 B1
return "zero"
var healed: int = _enemy_system.heal(_enemy_system.hero_unit(), int(snap.power)) # clamp 归 战斗结算
_emit_row("heal", source_class, cell, ri, "%s 回复%s" % [snap.display_name, suffix], healed, {"target": "hero"}, highlight)
return true
return false
return "redeemed"
return "not_instant"
func _judge_flag(evt: Dictionary, ri: int) -> void:
var cell: Vector2i = evt.cell
if evt.hit:
if _flag_consumed.has(cell):
return # 去重跳过(资格以雷格实例为键——A2 诚实重复上报的对侧);零明细行
return # 去重跳过(资格以雷格实例为键——排雷玩法 诚实重复上报的对侧);零明细行
_flag_consumed[cell] = true
_perfect_hit_true += 1
_charge_dmg_b += int(db.settlement.damage_b_per_flag) # 伤害B 充能:与 weaken 消费同键(每雷实例一次——忘拔旗不重复充能)
var ok: bool = _b3.apply_effect("weaken", {"source_class": "flag_hit", "origin_cell": cell, "round_index": ri})
var ok: bool = _status_effects.apply_effect("weaken", {"source_class": "flag_hit", "origin_cell": cell, "round_index": ri})
if ok:
var row: Dictionary = db.status_effect_row("weaken")
_emit_row("effect", "flag_hit", cell, ri, String(row.display_name), int(row.magnitude), {"effect_id": "weaken", "pool_value": _pool_effective("weaken")}, "flag_hit")
@@ -357,7 +379,7 @@ func _apply_miss_backlash(cell: Vector2i, ri: int) -> void:
var src: Dictionary = {"source_class": "flag_miss", "origin_cell": cell, "round_index": ri}
match kind:
0:
if _b3.apply_effect("enrage", src):
if _status_effects.apply_effect("enrage", src):
var row: Dictionary = db.status_effect_row("enrage")
_emit_row("effect", "flag_miss", cell, ri, "反噬·%s(敌方)" % String(row.display_name), int(row.magnitude), {"effect_id": "enrage", "pool_value": _pool_effective("enrage")})
else:
@@ -365,14 +387,14 @@ func _apply_miss_backlash(cell: Vector2i, ri: int) -> void:
1:
var granted := 0
for i in range(2):
if _b3.apply_effect("ambush", src):
if _status_effects.apply_effect("ambush", src):
granted += 1
if granted > 0:
_emit_row("effect", "flag_miss", cell, ri, "反噬·敌方伏击 +%d 层" % granted, granted, {"effect_id": "ambush", "pool_value": _b3.pool_layers("ambush") * _m_amb})
_emit_row("effect", "flag_miss", cell, ri, "反噬·敌方伏击 +%d 层" % granted, granted, {"effect_id": "ambush", "pool_value": _status_effects.pool_layers("ambush") * _m_amb})
else:
_emit_row("reward_ineffective", "flag_miss", cell, ri, "反噬未生效(满层)", 0, {"batch_key": "ambush"})
2:
if _b3.apply_effect("off_guard", src):
if _status_effects.apply_effect("off_guard", src):
var row2: Dictionary = db.status_effect_row("off_guard")
_emit_row("effect", "flag_miss", cell, ri, "反噬·%s(我方)" % String(row2.display_name), int(row2.magnitude), {"effect_id": "off_guard", "pool_value": _pool_effective("off_guard")})
else:
@@ -380,13 +402,13 @@ func _apply_miss_backlash(cell: Vector2i, ri: int) -> void:
func _convert_ambush(mine_cells: Array, ri: int) -> void:
# B2 义务②⑥:预截流计划面 → 前 granted 颗逐颗转化(事件规范序);每颗雷恰一次
var plan: Dictionary = _b2.ambush_grant_plan(mine_cells.size(), _enemy_cap)
# 敌人系统 义务②⑥:预截流计划面 → 前 granted 颗逐颗转化(事件规范序);每颗雷恰一次
var plan: Dictionary = _enemy_system.ambush_grant_plan(mine_cells.size(), _enemy_cap)
for i in range(plan.granted):
var cell: Vector2i = mine_cells[i]
var ok: bool = _b3.apply_effect("ambush", {"source_class": "mine_hit", "origin_cell": cell, "round_index": ri})
var ok: bool = _status_effects.apply_effect("ambush", {"source_class": "mine_hit", "origin_cell": cell, "round_index": ri})
if ok:
var layers := _b3.pool_layers("ambush")
var layers := _status_effects.pool_layers("ambush")
_emit_row("effect", "mine_hit", cell, ri, "伏击", _m_amb, {"effect_id": "ambush", "pool_value": layers * _m_amb})
for i in range(plan.granted, plan.granted + plan.overflow):
var cell: Vector2i = mine_cells[i]
@@ -394,25 +416,37 @@ func _convert_ambush(mine_cells: Array, ri: int) -> void:
func _grant_clear_reward(ri: int) -> void:
var snap = _b4.grant_clear()
if _board_from_midround_refresh:
return # 轮内刷新换来的新图不发清图奖励(行动经济/战斗结算 明文——刷新图不是清图挣来的)
var snap = _reward_economy.grant_clear()
if not (snap is Dictionary):
return
var perfect: bool = _perfect_hit_false == 0 and _perfect_hit_true >= 1
# 首份兑付
# 首份兑付(first_ok = 实际有效兑付——零兑付/被拒均不算)
var first_ok := _grant_clear_identity(snap, ri)
# 完美排雷:真实双份交付(第二份走既有授予通道;grant_clear 本身仍恰一次)
# 完美排雷:真实双份交付(第二份按身份类型走真实兑付——即时类重走 _resolve_instant;
# 增益类重授容器;道具类入库。grant_clear 本身仍恰一次)
var second_ok := false
if perfect and first_ok:
if snap.category == "buff":
second_ok = _b3.apply_effect(snap.effect_id, {"source_class": "clear_reward", "origin_cell": Vector2i(-1, -1), "round_index": ri})
second_ok = _status_effects.apply_effect(snap.effect_id, {"source_class": "clear_reward", "origin_cell": Vector2i(-1, -1), "round_index": ri})
if second_ok:
var row: Dictionary = db.status_effect_row(snap.effect_id)
_emit_row("effect", "clear_reward", Vector2i(-1, -1), ri, "%s(完美第二份)" % String(row.display_name), int(row.magnitude), {"effect_id": snap.effect_id, "pool_value": _pool_effective(snap.effect_id)})
else:
_emit_row("reward_ineffective", "clear_reward", Vector2i(-1, -1), ri, "完美第二份未生效(满层)", 0, {"batch_key": String(snap.effect_id)})
else:
var s2 = _b4.stock(snap, Vector2i(-1, -1), ri)
second_ok = s2 is Dictionary
if second_ok:
_emit_row("item", "clear_reward", Vector2i(-1, -1), ri, "%s 入库(完美第二份)" % snap.display_name, int(snap.power), {})
var res2 := _resolve_instant(snap, "clear_reward", Vector2i(-1, -1), ri, "(完美第二份)")
if res2 == "redeemed":
second_ok = true
elif res2 == "not_instant":
var s2 = _reward_economy.stock(snap, Vector2i(-1, -1), ri)
second_ok = s2 is Dictionary
if second_ok:
_emit_row("item", "clear_reward", Vector2i(-1, -1), ri, "%s 入库(完美第二份)" % snap.display_name, int(snap.power), {})
else:
_emit_row("reward_ineffective", "clear_reward", Vector2i(-1, -1), ri, "完美第二份未生效(入库被拒)", 0, {"batch_key": String(snap.reward_id)})
# res2 == "zero":零兑付的奖励未生效行已由 _resolve_instant 自记
if perfect and first_ok:
# 高光行 value = 实际有效兑付份数(禁记名义值;首份满层零兑付 → 降级不发)
_emit_row("highlight", "clear_reward", Vector2i(-1, -1), ri, "完美排雷!清图奖励 ×2", (2 if second_ok else 1), {}, "perfect_sweep")
@@ -420,17 +454,19 @@ func _grant_clear_reward(ri: int) -> void:
func _grant_clear_identity(snap: Dictionary, ri: int) -> bool:
if snap.category == "buff":
var ok: bool = _b3.apply_effect(snap.effect_id, {"source_class": "clear_reward", "origin_cell": Vector2i(-1, -1), "round_index": ri})
var ok: bool = _status_effects.apply_effect(snap.effect_id, {"source_class": "clear_reward", "origin_cell": Vector2i(-1, -1), "round_index": ri})
if ok:
var row: Dictionary = db.status_effect_row(snap.effect_id)
_emit_row("effect", "clear_reward", Vector2i(-1, -1), ri, String(row.display_name), int(row.magnitude), {"effect_id": snap.effect_id, "pool_value": _pool_effective(snap.effect_id)}, "clear_reward_h2")
return true
_emit_row("reward_ineffective", "clear_reward", Vector2i(-1, -1), ri, "清图奖励未生效(满层)", 0, {"batch_key": snap.effect_id})
return false
if _resolve_instant(snap, "clear_reward", Vector2i(-1, -1), ri, "(清图奖励)", "clear_reward_h2"):
return true
var s = _b4.stock(snap, Vector2i(-1, -1), ri)
var res := _resolve_instant(snap, "clear_reward", Vector2i(-1, -1), ri, "(清图奖励)", "clear_reward_h2")
if res != "not_instant":
return res == "redeemed" # 即时类零兑付(无存活敌/满血)= 未生效——首份即零兑付则高光行不发
var s = _reward_economy.stock(snap, Vector2i(-1, -1), ri)
if not (s is Dictionary):
_emit_row("reward_ineffective", "clear_reward", Vector2i(-1, -1), ri, "清图奖励未生效(入库被拒)", 0, {"batch_key": String(snap.reward_id)})
return false
_emit_row("item", "clear_reward", Vector2i(-1, -1), ri, "%s 入库(清图奖励)" % snap.display_name, int(snap.power), {}, "clear_reward_h2")
return true
@@ -443,29 +479,29 @@ func _apply_selected_item(window_index: int, ri: int) -> void:
return
var item: Dictionary = _window_items[window_index]
if item.category == "heal_pack":
# 逐件动态守卫:前件推满后本件拦截(不烧包——B1 不调 use)
if int(_b2.hero_unit().hp) >= int(_b2.hero_unit().max_hp):
# 逐件动态守卫:前件推满后本件拦截(不烧包——战斗结算 不调 use)
if int(_enemy_system.hero_unit().hp) >= int(_enemy_system.hero_unit().max_hp):
_emit_row("use_rejected", "item", item.source_cell, ri, "%s 被拒(满血)" % item.display_name, 0, {})
return
var receipt = _b4.use(item.index)
var receipt = _reward_economy.use(item.index)
if not (receipt is Dictionary):
return
var healed: int = _b2.heal(_b2.hero_unit(), receipt.power) # clamp 归 B1
var healed: int = _enemy_system.heal(_enemy_system.hero_unit(), receipt.power) # clamp 归 战斗结算
_emit_row("heal", "item", receipt.source_cell, ri, "%s 回复" % item.display_name, healed, {"target": "hero"})
else:
# 伤害道具:恒定表值不吃修正;零效果判定读 B2 存活集(照常消耗——E10)
if _b2.query_alive("enemy").is_empty():
var receipt0 = _b4.use(item.index)
# 伤害道具:恒定表值不吃修正;零效果判定读 敌人系统 存活集(照常消耗——E10)
if _enemy_system.query_alive("enemy").is_empty():
var receipt0 = _reward_economy.use(item.index)
if not (receipt0 is Dictionary):
return
_emit_row("consumed_ineffective", "item", receipt0.source_cell, ri, "已消耗未生效(无存活敌人)", 0, {})
return
var receipt = _b4.use(item.index)
var receipt = _reward_economy.use(item.index)
if not (receipt is Dictionary):
return
var target = _b2.default_target()
var target = _enemy_system.default_target()
var killed_before: bool = not target.alive
_b2.apply_damage(target, receipt.power)
_enemy_system.apply_damage(target, receipt.power)
var killed: bool = not killed_before and not target.alive
_emit_row("damage", "item", receipt.source_cell, ri, "%s 命中" % item.display_name, receipt.power, {"target": target.display_name, "killed": killed})
@@ -473,11 +509,11 @@ func _apply_selected_item(window_index: int, ri: int) -> void:
# ---- 战斗段 ----
func _combat_segment(ri: int) -> void:
var L := _b3.pool_layers("ambush") # 授予段后的当轮值;池共享不消耗
var L := _status_effects.pool_layers("ambush") # 授予段后的当轮值;池共享不消耗
# ⓪ 主角先手攻击(基础攻击恒存在——2026-09-11 用户裁决;先于敌方行动)。
# 三段伤害构成(2026-09-12 用户拍板):基础(atk×(1+attack_up),不吃克制)+ 伤害A(开格充能×克制)+ 伤害B(标雷充能×克制),
# 按目标护甲类型分别乘克制系数后 floor 相加;attack_up 仍只乘基础攻击(刀/AOE 恒定表值不吃修正不变)。
var hero_target = _b2.default_target()
var hero_target = _enemy_system.default_target()
if hero_target != null:
var armor: String = String(hero_target.get("armor_type", "none"))
var up := clampi(_pool_effective("attack_up"), int(db.settlement.pool_clamp_lo), int(db.settlement.pool_clamp_hi))
@@ -486,13 +522,17 @@ func _combat_segment(ri: int) -> void:
var comp_b := _charge_damage_component("dmg_b", _charge_dmg_b, armor)
var d_hero: int = base + comp_a + comp_b
if d_hero > 0:
_b2.apply_damage(hero_target, d_hero)
_enemy_system.apply_damage(hero_target, d_hero)
_emit_row("damage", "hero_attack", Vector2i(-1, -1), ri, "主角攻击", d_hero, {"target": hero_target.display_name, "killed": not hero_target.alive, "base": base, "dmg_a": comp_a, "dmg_b": comp_b})
for u in _b2.query_alive("enemy"):
if int(_b2.hero_unit().hp) <= 0:
else:
# 零值伤害行(战斗结算 边界表:"合成后为 0 时不调扣血接口,改写零值伤害行——免伤与零输出必须可见";
# 敌方侧同款口径已就绪——照抄。三分量字段齐备保 P2 归因不断链)
_emit_row("damage", "hero_attack", Vector2i(-1, -1), ri, "主角攻击(被减伤归零)", 0, {"target": hero_target.display_name, "base": base, "dmg_a": comp_a, "dmg_b": comp_b})
for u in _enemy_system.query_alive("enemy"):
if int(_enemy_system.hero_unit().hp) <= 0:
break # hero hp=0 立即短路(死尸挨打行是归因噪音——用户拍板)
var beat: String = _b2.query_beat(u)
var atk: Dictionary = _b2.query_attack(u, L, _m_amb) # 调用序①兑付读值
var beat: String = _enemy_system.query_beat(u)
var atk: Dictionary = _enemy_system.query_attack(u, L, _m_amb) # 调用序①兑付读值
if beat == "charge":
_emit_row("charge", "enemy_action", Vector2i(-1, -1), ri, "%s 蓄力(本轮踩雷白喂)" % u.display_name, 0, {"target": "hero"})
else:
@@ -500,9 +540,9 @@ func _combat_segment(ri: int) -> void:
if d_tick <= 0:
_emit_row("damage", "enemy_action", Vector2i(-1, -1), ri, "%s 攻击(被减伤归零)" % u.display_name, 0, {"target": "hero", "baseline": int(atk.baseline), "attach": int(atk.attach)})
else:
_b2.apply_damage(_b2.hero_unit(), d_tick) # 调用序②受击上报
_enemy_system.apply_damage(_enemy_system.hero_unit(), d_tick) # 调用序②受击上报
_emit_row("damage", "enemy_action", Vector2i(-1, -1), ri, "%s 攻击" % u.display_name, d_tick, {"target": "hero", "baseline": int(atk.baseline), "attach": int(atk.attach)})
_b2.advance(u) # 调用序③推进——每结算每存活敌恰一次(hero 不在推进域)
_enemy_system.advance(u) # 调用序③推进——每结算每存活敌恰一次(hero 不在推进域)
# F2 乘区:池内加算 → clamp [-100,+100] → 池间乘算 → floor(单一取整规则)——算术归 DamageMath(O3)
@@ -530,7 +570,7 @@ func _armor_effectiveness(component: String, armor: String) -> int:
func _clamped_pool(pool: String) -> int:
var total := 0
for eff in _b3.query_active("hero") + _b3.query_active("enemy"):
for eff in _status_effects.query_active("hero") + _status_effects.query_active("enemy"):
var row: Dictionary = db.status_effect_row(eff.effect_id)
if row.is_empty():
continue
@@ -544,16 +584,16 @@ func _pool_effective(effect_id: String) -> int:
var row: Dictionary = db.status_effect_row(effect_id)
if not row.is_empty():
side = String(row.target_side)
for eff in _b3.query_active(side):
for eff in _status_effects.query_active(side):
if eff.effect_id == effect_id:
return eff.effective_value
return 0
func _emit_composition_effects(side: String, ri: int) -> void:
for eff in _b3.query_active(side):
for eff in _status_effects.query_active(side):
if eff.round_index < ri:
# 续存实例(前轮授予)——每结算兑付一次的可见面(合并实例只持首授归因——B3 义务⑦)
# 续存实例(前轮授予)——每结算兑付一次的可见面(合并实例只持首授归因——增益状态 义务⑦)
_emit_row("effect", eff.source_class, eff.origin_cell, ri, eff.display_name, eff.snap_magnitude, {"effect_id": eff.effect_id, "pool_value": eff.effective_value})
@@ -561,12 +601,12 @@ func _emit_composition_effects(side: String, ri: int) -> void:
func _emit_settlement_snapshot(verdict: String) -> void:
var enemies: Array = []
for u in _b2.query_alive("enemy"):
for u in _enemy_system.query_alive("enemy"):
enemies.append({"id": String(u.enemy_id), "hp": int(u.hp), "max_hp": int(u.max_hp), "atk": int(u.atk), "beat": query_beat_of(u), "armor": String(u.get("armor_type", "none"))})
var buffs: Array = []
for eff in _b3.query_active("hero"):
for eff in _status_effects.query_active("hero"):
buffs.append({"id": eff.effect_id, "side": "hero", "layers": eff.layers, "left": eff.remaining_settlements})
for eff in _b3.query_active("enemy"):
for eff in _status_effects.query_active("enemy"):
buffs.append({"id": eff.effect_id, "side": "enemy", "layers": eff.layers, "left": eff.remaining_settlements})
var mines_hit := 0
var safe_n := 0
@@ -576,11 +616,11 @@ func _emit_settlement_snapshot(verdict: String) -> void:
elif evt.kind == "safe":
safe_n += 1
var board_stats := {"seq": _board_seq}
if a2.current_board() != null:
board_stats["w"] = a2.current_board().width
board_stats["h"] = a2.current_board().height
board_stats["revealed"] = a2.revealed_cells().size()
board_stats["flags"] = a2.flag_count()
if minesweeper.current_board() != null:
board_stats["w"] = minesweeper.current_board().width
board_stats["h"] = minesweeper.current_board().height
board_stats["revealed"] = minesweeper.revealed_cells().size()
board_stats["flags"] = minesweeper.flag_count()
settlement_snapshot.emit({
"kind": "settlement",
"level": int(encounter.get("level_index", 0)),
@@ -588,20 +628,20 @@ func _emit_settlement_snapshot(verdict: String) -> void:
"round": round_index,
"verdict": verdict,
"unit_count": enemies.size(),
"spawn_count": _b2.all_enemies().size(), # 本战斗实例化总数(含死亡残位)——unit_count=0 时区分「首轮全灭」vs「空场」(playtest-2026-09-12 报告 Bug#1 误读教训)
"hero": {"hp": int(_b2.hero_unit().get("hp", 0)), "max_hp": int(_b2.hero_unit().get("max_hp", 0)), "atk": int(_b2.hero_unit().get("atk", 0))},
"economy": {"phase": String(a3.phase), "ap": int(a3.ap), "rap": int(a3.rap), "refresh_used": _refresh_used_this_round},
"spawn_count": _enemy_system.all_enemies().size(), # 本战斗实例化总数(含死亡残位)——unit_count=0 时区分「首轮全灭」vs「空场」(playtest-2026-09-12 报告 Bug#1 误读教训)
"hero": {"hp": int(_enemy_system.hero_unit().get("hp", 0)), "max_hp": int(_enemy_system.hero_unit().get("max_hp", 0)), "atk": int(_enemy_system.hero_unit().get("atk", 0))},
"economy": {"phase": String(action_economy.phase), "ap": int(action_economy.ap), "rap": int(action_economy.rap), "refresh_used": _refresh_used_this_round},
"board": board_stats,
"enemies": enemies,
"buffs": buffs,
"ambush_layers": _b3.pool_layers("ambush"),
"inventory_in_stock": _b4.in_stock_count(),
"ambush_layers": _status_effects.pool_layers("ambush"),
"inventory_in_stock": _reward_economy.in_stock_count(),
"round_facts": {"path": _facts_path, "safe": safe_n, "mines": mines_hit, "flag_events": _facts_flags.size(), "charge_dmg_a": _charge_dmg_a, "charge_dmg_b": _charge_dmg_b},
})
func query_beat_of(u: Dictionary) -> String:
return _b2.query_beat(u)
return _enemy_system.query_beat(u)
# ---- 出口分发与战斗结束序列 ----
@@ -609,77 +649,67 @@ func query_beat_of(u: Dictionary) -> String:
func _exit_distribution() -> void:
var mode := "same_board"
if _facts_path == "cleared":
mode = "new_board" # cleared 轮恒走 new_board(契约义务)
elif _refresh_used_this_waiting:
mode = "new_board" # stopped/ap_exhausted 轮:仅 Waiting 期玩家刷新意图达成时换图(违约 fail-fast 的对侧)
mode = "new_board" # cleared 轮恒走 new_board(契约义务);非清图轮恒同图(等待期刷新通道已移除)
if mode == "new_board":
_b4.retire_mapping()
a2.retire_board()
_reward_economy.retire_mapping()
minesweeper.retire_board()
if not _install_new_board():
_fatal("换图编排失败")
return
a3.notify_settlement_done(mode)
a3.notify_round_start()
action_economy.notify_settlement_done(mode)
action_economy.notify_round_start()
_reset_round_facts()
_refresh_used_this_waiting = false
_refresh_used_this_round = 0
round_index += 1
round_started.emit(round_index)
# 呈现层查询面(HUD 刷新按钮可用性)
# 呈现层查询面(HUD 刷新按钮可用性)——挖掘期唯一通道
func can_request_refresh() -> bool:
return battle_result == "" \
and _refresh_used_this_round < int(db.action_economy.refresh_max_per_round) \
and (a3.phase == a3.PHASE_ACTIVE or a3.phase == a3.PHASE_WAITING) \
and (a3.phase != a3.PHASE_WAITING or not _refresh_used_this_waiting) \
and a3.rap >= int(db.action_economy.rap_refresh_cost)
and action_economy.phase == action_economy.PHASE_ACTIVE \
and action_economy.rap >= int(db.action_economy.rap_refresh_cost)
# 玩家经 D1 发起、B1 代理调用(禁止 RAP 满自动刷)。
# 轮内刷新(2026-09-12 方案 A 用户拍板):Active 相位立即换图继续挖(RAP -8;AP 保留、相位不动、
# 不触发结算);Waiting 相位保持现状(登记意图,exit_distribution 换图)。每轮上限 = 表值(两相位合计)。
# 玩家经 HUD框架发起、战斗结算 代理调用(禁止 RAP 满自动刷)。
# 挖掘期唯一通道(2026-09-12 方案 A 拍板 + 2026-09-15 等待期通道整体移除):Active 相位立即换图
# 继续挖(RAP -8;AP 保留、相位不动、不触发结算);非挖掘期请求一律拒绝——非清图轮恒同图。
func player_request_refresh() -> bool:
if not can_request_refresh():
return false
if a3.phase == a3.PHASE_ACTIVE:
if not a3.request_refresh_active():
return false
_refresh_used_this_round += 1
_b4.retire_mapping() # 旧图批次退役(B4 Ready→retired;E6 新批重置前提)
a2.retire_board(true) # 旧板退役(轮内通道)
return _install_new_board(true) # 当场换图——失败即编排违例 fail-fast
if not a3.request_refresh():
if not action_economy.request_refresh_active():
return false
_refresh_used_this_round += 1
_refresh_used_this_waiting = true
return true
_reward_economy.retire_mapping() # 旧图批次退役(奖励道具经济 Ready→retired;E6 新批重置前提)
minesweeper.retire_board(true) # 旧板退役(轮内通道)
return _install_new_board(true) # 当场换图——失败即编排违例 fail-fast
func _battle_over(result: String) -> void:
battle_result = result
# 库存关卡域(2026-09-11 关内延续):battle_over 不再清库存——跨战斗延续,关卡末经 level_clear() 清空;
# 映射随战斗结束退役(下一场战斗 = 新板新 scope 批,roll_board 相位守卫的前提)
var hero_hp_left := int(_b2.hero_unit().get("hp", 0)) # despawn 前捕获(信号载荷供呈现层"HP 带入下一层")
_b4.retire_mapping()
_b2.despawn_all()
_b3.notify_battle_end()
a3.notify_combat_end() # → Idle,RAP 清零
var hero_hp_left := int(_enemy_system.hero_unit().get("hp", 0)) # despawn 前捕获(信号载荷供呈现层"HP 带入下一层")
_reward_economy.retire_mapping()
_enemy_system.despawn_all()
_status_effects.notify_battle_end()
action_economy.notify_combat_end() # → Idle,RAP 清零
db.set_reload_gate(false)
_reset_round_facts()
battle_finished.emit({"result": result, "hero_hp": hero_hp_left, "remaining_items": _b4.in_stock_count()})
battle_finished.emit({"result": result, "hero_hp": hero_hp_left, "remaining_items": _reward_economy.in_stock_count()})
# ---- 呈现状态面(E1 封装收口 2026-09-13):呈现层 / headless 自动玩家的唯一读取出口——
# 协作对象(_b2/_b3/_b4)私有化后,跨层读取一律经本快照或下列命名面(diagnostic_snapshot 同款模式,
# 禁再链式穿透 b1.b2.hero 一类路径)。载荷 schema 成文(code-conventions 2.1):
# 协作对象(_enemy_system/_status_effects/_reward_economy)私有化后,跨层读取一律经本快照或下列命名面(diagnostic_snapshot 同款模式,
# 禁再链式穿透 settlement.enemy_system.hero 一类路径)。载荷 schema 成文(code-conventions 2.1):
# round / battle_result / phase / ap / rap / can_request_refresh / hero{hp,max_hp,atk,alive} /
# enemies[{order,display_name,hp,max_hp,enemy_id,armor_type,beat,ring_size,ring_index,ambush_layers,alive}] /
# ambush_layers / hero_effects[](B3 query_active 投影) / inventory[](B4 query_inventory 投影) / in_stock
# ambush_layers / hero_effects[](增益状态 query_active 投影) / inventory[](奖励道具经济 query_inventory 投影) / in_stock
func presentation_snapshot() -> Dictionary:
var enemies: Array = []
var all: Array = _b2.all_enemies()
var ambush: int = _b3.pool_layers("ambush")
var all: Array = _enemy_system.all_enemies()
var ambush: int = _status_effects.pool_layers("ambush")
for i in all.size():
var u: Dictionary = all[i]
var alive: bool = bool(u.get("alive", false))
@@ -690,44 +720,44 @@ func presentation_snapshot() -> Dictionary:
"hp": int(u.hp), "max_hp": int(u.max_hp),
"enemy_id": String(u.enemy_id),
"armor_type": String(u.get("armor_type", "none")),
"beat": String(_b2.query_beat(u)) if alive else "",
"beat": String(_enemy_system.query_beat(u)) if alive else "",
"ring_size": ring.size(),
"ring_index": int(u.get("steps", 0)) % maxi(ring.size(), 1),
"ambush_layers": ambush,
"alive": alive,
})
var hero := {}
var h: Dictionary = _b2.hero_unit()
var h: Dictionary = _enemy_system.hero_unit()
if not h.is_empty():
hero = {"hp": int(h.hp), "max_hp": int(h.max_hp), "atk": int(h.atk), "alive": bool(h.get("alive", false))}
return {
"round": round_index,
"battle_result": battle_result,
"phase": String(a3.phase),
"ap": int(a3.ap), "rap": int(a3.rap),
"phase": String(action_economy.phase),
"ap": int(action_economy.ap), "rap": int(action_economy.rap),
"can_request_refresh": can_request_refresh(),
"hero": hero,
"enemies": enemies,
"ambush_layers": ambush,
"hero_effects": _b3.query_active("hero"),
"inventory": _b4.query_inventory(),
"in_stock": _b4.in_stock_count(),
"hero_effects": _status_effects.query_active("hero"),
"inventory": _reward_economy.query_inventory(),
"in_stock": _reward_economy.in_stock_count(),
}
# 战斗进行中谓词(hero 在场且存活)——组合根高频相位判断的低成本面
func is_battle_live() -> bool:
return battle_result == "" and not _b2.hero_unit().is_empty()
return battle_result == "" and not _enemy_system.hero_unit().is_empty()
# 库存余量(层间过渡文案 / 终局清库反馈)
func stock_count() -> int:
return _b4.in_stock_count()
return _reward_economy.in_stock_count()
# 奖励身份解析(组合根注入 BoardView 徽章回调的只读代理——Ready 相位守卫归 B4)
# 奖励身份解析(组合根注入 BoardView 徽章回调的只读代理——Ready 相位守卫归 奖励道具经济)
func query_reward_identity(cell: Vector2i):
return _b4.query_identity(cell)
return _reward_economy.query_identity(cell)
# 诊断观测面(组合根诊断转储 / 行为探针消费):私有编排状态的成文只读出口——
@@ -738,26 +768,25 @@ func diagnostic_snapshot() -> Dictionary: return {
"settling": _settling,
"awaiting_window": _awaiting_window,
"refresh_used_round": _refresh_used_this_round,
"refresh_used_waiting": _refresh_used_this_waiting,
"board_seq": _board_seq,
"facts_path": _facts_path,
"facts_reveals": _facts_reveals.size(),
"facts_flags": _facts_flags.size(),
"a3": {"phase": String(a3.phase), "ap": a3.ap, "rap": a3.rap},
"a2_board_installed": a2.current_board() != null,
"a2_revealed": a2.revealed_cells().size() if a2.current_board() != null else 0,
"a2_flags": a2.flag_count() if a2.current_board() != null else 0,
"b4_phase_stock": [_b4.in_stock_count()],
"enemies_alive": _b2.query_alive("enemy").size(),
"hero_hp": int(_b2.hero_unit().get("hp", -1)) if not _b2.hero_unit().is_empty() else -1,
"action_economy": {"phase": String(action_economy.phase), "ap": action_economy.ap, "rap": action_economy.rap},
"minesweeper_board_installed": minesweeper.current_board() != null,
"minesweeper_revealed": minesweeper.revealed_cells().size() if minesweeper.current_board() != null else 0,
"minesweeper_flags": minesweeper.flag_count() if minesweeper.current_board() != null else 0,
"reward_economy_phase_stock": [_reward_economy.in_stock_count()],
"enemies_alive": _enemy_system.query_alive("enemy").size(),
"hero_hp": int(_enemy_system.hero_unit().get("hp", -1)) if not _enemy_system.hero_unit().is_empty() else -1,
}
# 关卡结束编排(C1/C2 关卡结构落位前由上层显式调用——2026-09-11 关内延续):
# 关卡结束编排(关卡节点地图/冒险结构 关卡结构落位前由上层显式调用——2026-09-11 关内延续):
# 清空库存并返回快照计数("剩余道具 ×N 随关卡结束清空"反馈行 / 折算结构位的数据面)
func level_clear() -> Dictionary:
var remaining := _b4.in_stock_count()
_b4.clear_inventory()
var remaining := _reward_economy.in_stock_count()
_reward_economy.clear_inventory()
return {"remaining": remaining}
@@ -772,6 +801,7 @@ func _reset_round_facts() -> void:
# ---- 行协议(Core Rule 14:公共字段 + 行类型特有字段)----
func _emit_row(type: String, source_class: String, origin_cell: Vector2i, ri: int, display: String, value: int, extra: Dictionary, highlight: String = "") -> void:
_assert_row_source(type, source_class, origin_cell)
var row := {
"type": type,
"source_class": source_class,
@@ -787,6 +817,19 @@ func _emit_row(type: String, source_class: String, origin_cell: Vector2i, ri: in
step_row.emit(row)
# 行来源断言(战斗结算 明细流水:"任何一行找不到来源即断言失败——宁可报错"):
# 来源字段 = source_class(公共字段)+ origin_cell(棋盘来源行的真实格)。
# 豁免按两层成文(见 SOURCE_EXEMPT_TYPES / SOURCE_EXEMPT_CLASSES 常量注释),其余行必须带真实棋盘格。
func _assert_row_source(type: String, source_class: String, origin_cell: Vector2i) -> void:
if SOURCE_EXEMPT_TYPES.has(type):
return
assert(source_class != "", "[战斗结算] 明细行缺来源类别(type=%s)——任何一行找不到来源即断言失败" % type)
if SOURCE_EXEMPT_CLASSES.has(source_class):
return
assert(origin_cell.x >= 0 and origin_cell.y >= 0,
"[战斗结算] 明细行来源格越界(type=%s source=%s cell=%s)——棋盘来源行必须指回真实格" % [type, source_class, str(origin_cell)])
func _fatal(msg: String) -> void:
push_error("[B1/fatal] " + msg)
push_error("[战斗结算/fatal] " + msg)
assert(false, msg)
@@ -1,8 +1,8 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
# Date: 2026-09-10
# A1 棋盘产物数据契约(A2 消费;生成后不可变——A2 持快照即持活引用,生成器不再触碰)。
# 实现 GDD:design/archive/gdd-2026-09-14/a1-board-generation.md(系统 ID A1——registry 追溯锚,命名规范见 CLAUDE.md)
# 棋盘产物数据契约(排雷玩法消费;生成后不可变——排雷玩法持快照即持活引用,生成器不再触碰)。
# 实现 GDD:design/archive/gdd-2026-09-14/a1-board-generation.md(系统 ID A1·棋盘生成——registry 追溯锚,命名规范见 CLAUDE.md)
class_name BoardData
extends RefCounted
@@ -15,8 +15,9 @@ var variants := {} # Vector2i → Array[String](生成期快照,抗表
var biome := ""
var level_index: int = 1
var node_depth: int = 0
var metrics := {} # 八项生成期指标
var illegal_product := false
var metrics := {} # 生成期指标集(键清单单点定义在 board_generator._finalize)
var illegal_product := false # 非法产物标记(接受最不坏出口——零合法板诚实降级)
var degraded_product := false # 降级产物标记(接受最优出口——有合法板但无一达标;与 illegal_product 对称)
func in_bounds(c: Vector2i) -> bool:
@@ -93,7 +94,7 @@ func fog_cells() -> Array:
return out
# R₀ = 出生 3×3 ∪ (出生 3×3 经 0 格的 flood 闭包)——与 A2 F3 逐字同构(A1 F1 求解器初始态)。
# R₀ = 出生 3×3 ∪ (出生 3×3 经 0 格的 flood 闭包)——与 排雷玩法 F3 逐字同构(棋盘生成 F1 求解器初始态)。
func compute_r0() -> Array:
var b3 := spawn_cells()
var r0 := {}
@@ -110,7 +111,7 @@ func compute_r0() -> Array:
return r0.keys()
# A1 Rule 4 排除域(以最终产物板计):R₀ ∪ 外扩 1 环——生成期确定性计算。
# 棋盘生成 Rule 4 排除域(以最终产物板计):R₀ ∪ 外扩 1 环——生成期确定性计算。
func exclusion_domain() -> Dictionary:
var r0 := compute_r0()
var dom := {}
@@ -1,10 +1,10 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
# Date: 2026-09-10
# A1 确定性棋盘生成器:五阶段管线(参数→布雷→变异→验证循环→产出)+ F1 可推理率度量
# 确定性棋盘生成器:五阶段管线(参数→布雷→变异→验证循环→产出)+ F1 可推理率度量
# (single_subset 求解器)+ F3 雾预算 + F5 验收算子(达标即出/接受最优/接受最不坏)。
# 切片裁剪登记:黄金向量回归套件与 F4 批量工具不做(C3 调参期工具域)。
# 实现 GDD:design/archive/gdd-2026-09-14/a1-board-generation.md(系统 ID A1——registry 追溯锚,命名规范见 CLAUDE.md)
# 切片裁剪登记:黄金向量回归套件与 F4 批量工具不做(难度曲线与Boss 调参期工具域)。
# 实现 GDD:design/archive/gdd-2026-09-14/a1-board-generation.md(系统 ID A1·棋盘生成——registry 追溯锚,命名规范见 CLAUDE.md)
class_name BoardGenerator
extends RefCounted
@@ -52,7 +52,7 @@ func generate(L: int, D: int, rng_stream: RandomNumberGenerator, db) -> BoardDat
if best_legal == null or inf > best_legal_score:
best_legal = b
best_legal_score = inf
if inf >= params.target and m["inferability_core"] >= params.target:
if inf >= params.target: # 单一判据(棋盘生成:现行规则已回归"可推理率 ≥ 及格线")
accepted = b
accept_mode = "first_pass"
break
@@ -60,12 +60,13 @@ func generate(L: int, D: int, rng_stream: RandomNumberGenerator, db) -> BoardDat
if best_legal != null:
accepted = best_legal
accept_mode = "accept_best"
push_warning("[A1] 重试耗尽无达标——接受最优出图(强告警埋点,target=%.2f best=%.3f)" % [params.target, best_legal_score])
accepted.degraded_product = true # 降级标记(独立于 illegal_product——三分出口表要求标记随产物上抛)
push_warning("[棋盘生成] 重试耗尽无达标——接受最优出图(强告警埋点,target=%.2f best=%.3f)" % [params.target, best_legal_score])
else:
accepted = best_bad
accept_mode = "accept_worst"
accepted.illegal_product = true
push_error("[A1] attempts 耗尽零合法——接受最不坏出图 + 非法产物标记(玩家永不卡死)")
push_error("[棋盘生成] attempts 耗尽零合法——接受最不坏出图 + 非法产物标记(玩家永不卡死)")
_finalize(accepted, params, attempts_used, accept_mode, db)
return accepted
@@ -275,7 +276,7 @@ func measure_inferability(b: BoardData) -> Dictionary:
for con in derived:
if con.mines == 0:
for c in con.unknown:
if safe_set.has(c) and not r.has(c):
if not r.has(c): # 同 no-peek 口径:推导结论不再用 safe_set 复核(约束本身可靠)
r[c] = true
changed = true
elif con.mines == con.unknown.size():
@@ -313,8 +314,8 @@ func _build_constraints(b: BoardData, r: Dictionary, known_mines: Dictionary, sa
for n in b.neighbors8(c):
if known_mines.has(n):
known_here += 1
elif not r.has(n) and safe_set.has(n):
unknown[n] = true
elif not r.has(n):
unknown[n] = true # 未揭示格一律计入未知(含雷格)——求解器禁读 safe_set 真值(棋盘生成:雷位不许偷看)
var d_eff := d - known_here
if d_eff < 0 or (d_eff > 0 and unknown.is_empty()):
continue # 矛盾态(非法板)——不产约束,验收由 legality 闸承担
@@ -413,7 +414,7 @@ func legality(b: BoardData, params: Dictionary, db) -> bool:
func _legality_faults(b: BoardData, params: Dictionary) -> Array:
var faults: Array = []
# ① 出生区无雷 ② 出生区∪R₀ 无变异 ③ 数字恒真 ④ R₀ ≠ S(A2 修订义务)
# ① 出生区无雷 ② 出生区∪R₀ 无变异 ③ 数字恒真 ④ R₀ ≠ S(排雷玩法 修订义务)
for c in b.spawn_cells():
if b.is_mine(c):
faults.append("inv1_mine_in_spawn")
@@ -477,12 +478,16 @@ func _has_all_fog_window(b: BoardData, fog: Array) -> bool:
return false
# ==================== 产出上报(八项指标) ====================
# ==================== 产出上报(生成期指标集) ====================
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)
b.metrics = {
"metric_version": METRIC_VERSION,
@@ -490,16 +495,20 @@ func _finalize(b: BoardData, params: Dictionary, attempts_used: int, accept_mode
"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"],
}
b.illegal_product = accept_mode == "accept_worst"
b.degraded_product = accept_mode == "accept_best"
func _badness(b: BoardData, params: Dictionary) -> Array:
@@ -537,5 +546,5 @@ func _lex_less(a: Array, b: Array) -> bool:
func _fatal(msg: String) -> void:
push_error("[A1/fatal] " + msg)
push_error("[棋盘生成/fatal] " + msg)
assert(false, msg)
@@ -1,9 +1,9 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
# Date: 2026-09-10
# D2 ConfigDB autoload——数值表单点访问入口(Rule 2)+ 校验双通道(Rule 6)+ R1 行解析。
# 实现 GDD:design/archive/gdd-2026-09-14/d2-data-config.md(系统 ID D2——registry 追溯锚,命名规范见 CLAUDE.md)
# 切片注:headless --script 模式下由冒烟脚本显式实例化(D2 AC 前言);玩法代码禁止绕过本单点自建 load()。
# ConfigDB autoload(数据配置)——数值表单点访问入口(Rule 2)+ 校验双通道(Rule 6)+ R1 行解析。
# 实现 GDD:design/archive/gdd-2026-09-14/d2-data-config.md(系统 ID D2·数据配置——registry 追溯锚,命名规范见 CLAUDE.md)
# 切片注:headless --script 模式下由冒烟脚本显式实例化(数据配置 AC 前言);玩法代码禁止绕过本单点自建 load()。
extends Node
const SCHEMA_VERSION := 1
@@ -37,7 +37,7 @@ var difficulty: DifficultyTable
var is_ready := false
var _diagnostics: Array = [] # {severity, category, message}
var _warn_dedup: Dictionary = {} # R1 四元组去重注册表
var _reload_gate := false # D2 EC8——B1 唯一调用方
var _reload_gate := false # 数据配置 EC8——战斗结算 唯一调用方
func push_diag(severity: String, category: String, message: String) -> void:
@@ -148,7 +148,7 @@ func _cross_validate(loaded: Dictionary) -> void:
has_buff = true
if safe_rows.size() > 0 and not has_buff:
push_diag("error", "no_live_buff_row", "safe_cell 池零活 buff 行——tier_min_buff 无定义")
# ③ clear_default 恒正当量(A3 条款④——2026-09-11 供给控制器批:清图槽改即时类一次性伤害,正当量含即时类)
# ③ clear_default 恒正当量(行动经济 条款④——2026-09-11 供给控制器批:清图槽改即时类一次性伤害,正当量含即时类)
var clear_value_ok := false
for row in clear_rows:
if row.get("category", "") == "buff" and String(row.get("effect_id", "")) != "":
@@ -188,7 +188,7 @@ func _cross_validate(loaded: Dictionary) -> void:
for eff in st_tab.effect_pool_map.keys():
if not effect_ids.has(eff):
push_diag("error", "poolmap_dangling", "SettlementTable.effect_pool_map 悬空 effect_id: %s" % eff)
# ⑦ source_label_map 键集封闭性(键 = B3 source_class ∪ {item, enemy_action, flag_miss})
# ⑦ source_label_map 键集封闭性(键 = 增益状态 source_class ∪ {item, enemy_action, flag_miss})
var required_sources := ["safe_cell", "flag_hit", "mine_hit", "clear_reward", "item", "hero_attack", "enemy_action", "flag_miss"]
for s in required_sources:
if not st_tab.source_label_map.has(s):
@@ -228,7 +228,7 @@ func resolve_enemy_rowset(L: int, D: int) -> Dictionary:
return _resolve_progress(enemy.rows, L, D, "enemy")
# C3 DifficultyTable 消费面(B2.spawn 调用):(L,D) 精确匹配修饰乘数,越域值 clamp(GDD Edge Case——组合事故非配置事故)
# 难度曲线与Boss DifficultyTable 消费面(敌人系统.spawn 调用):(L,D) 精确匹配修饰乘数,越域值 clamp(GDD Edge Case——组合事故非配置事故)
func resolve_difficulty_modifiers(L: int, D: int) -> Dictionary:
if difficulty == null:
return {}
@@ -303,7 +303,7 @@ func _resolve_progress(rows: Array, L: int, D: int, table_id: String) -> Diction
return {}
# ---- A4 variant_pool_gate:候选池门控(∅ biome_tag = 通配)----
# ---- 地形变异 variant_pool_gate:候选池门控(∅ biome_tag = 通配)----
func variant_pool(level_index: int, board_biomes: Array) -> Array:
var pool: Array = []
@@ -325,7 +325,7 @@ func status_effect_row(effect_id: String) -> Dictionary:
return status_effects.row_by_id(effect_id)
# ---- D2 EC8:debug 热重载门(B1 唯一调用方;切片实现 = 门标志 + 拒绝语义)----
# ---- 数据配置 EC8:debug 热重载门(战斗结算 唯一调用方;切片实现 = 门标志 + 拒绝语义)----
func set_reload_gate(gated: bool) -> void:
_reload_gate = gated
@@ -333,7 +333,7 @@ func set_reload_gate(gated: bool) -> void:
func reload() -> bool:
if _reload_gate:
push_diag("warning", "reload_gated", "战斗中热重载被拒绝(D2 EC8)")
push_diag("warning", "reload_gated", "战斗中热重载被拒绝(数据配置 EC8)")
return false
var ok := load_tables("res://data/tables", false)
if ok:
@@ -1,8 +1,8 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
# 实现 GDD:design/archive/gdd-2026-09-14/b1-settlement.md(系统 ID B1——registry 追溯锚,命名规范见 CLAUDE.md)
# B1 战斗段伤害公式族(零状态纯算术宿主——规范 1.2「工具性纯函数挂明确宿主类」;
# 自 battle_orchestrator 拆出 2026-09-13,封装审查 O3)。池读取与表值读取仍归 B1,本类只做算术。
# 实现 GDD:design/archive/gdd-2026-09-14/b1-settlement.md(系统 ID B1·战斗结算——registry 追溯锚,命名规范见 CLAUDE.md)
# 战斗结算·战斗段伤害公式族(零状态纯算术宿主——规范 1.2「工具性纯函数挂明确宿主类」;
# 自 battle_orchestrator 拆出 2026-09-13,封装审查 O3)。池读取与表值读取仍归 战斗结算,本类只做算术。
class_name DamageMath
extends RefCounted
@@ -1,19 +1,19 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 线性第一关(战×2→道具→战×2→恢复→Boss)能否承载"数值提升→通关期望改善"的元进度验证
# Date: 2026-09-11
# C1 关卡节点地图(provisional 线性形态——节点链来自 LevelTable,路径分支/形态定稿权归 C1):
# 关卡域生命周期唯一编排者——共享 B4 容器跨战斗延续(2026-09-11 关内延续裁决)、HP 跨战斗携带、
# 奖励点/恢复点节点兑付、关卡末 level_clear 清库。Core 层零 UI 依赖(headless 可跑完整关卡——D2 Rule 7)。
# 测试专用:hero_atk_pct/hero_hp_pct 数值覆盖(调整板模拟天赋树——正式侧归 C4 元进度,切片不落盘)。
# 关卡节点地图(provisional 线性形态——节点链来自 LevelTable,路径分支/形态定稿权归 关卡节点地图):
# 关卡域生命周期唯一编排者——共享 奖励道具经济 容器跨战斗延续(2026-09-11 关内延续裁决)、HP 跨战斗携带、
# 奖励点/恢复点节点兑付、关卡末 level_clear 清库。Core 层零 UI 依赖(headless 可跑完整关卡——数据配置 Rule 7)。
# 测试专用:hero_atk_pct/hero_hp_pct 数值覆盖(调整板模拟天赋树——正式侧归元进度,切片不落盘)。
# E4 收口(2026-09-13):覆盖值经 encounter 显式传入战斗(不再写 db.hero 表——表值运行期不可变,
# 双基线同步/双写叠加 6→18→54 整类隐患随表写一并移除)。
# 对应 GDD:C1 关卡节点地图(未设计——provisional 切片先行,定稿后回写;命名规范见 CLAUDE.md)
# 对应 GDD:关卡节点地图(未设计——provisional 切片先行,定稿后回写;命名规范见 CLAUDE.md)
class_name LevelRunner
extends RefCounted
signal node_entered(node: Dictionary, info: Dictionary) # info 按 type:battle/boss={} · reward={snapshot} · rest={healed,hp,max_hp}
signal step_row(row: Dictionary) # 转发当前 B1 结算行
signal window_opened(items: Array) # 转发当前 B1 道具窗口
signal step_row(row: Dictionary) # 转发当前 战斗结算 结算行
signal window_opened(items: Array) # 转发当前 战斗结算 道具窗口
signal round_started(round_index: int)
signal board_changed()
signal level_finished(payload: Dictionary) # {result, hp, node_index, node_type, remaining_items}
@@ -23,9 +23,9 @@ var rng
var level_row: Dictionary = {}
var node_index := -1
var hero_hp := 0
# 协作对象私有(E1/E3 封装收口 2026-09-13):b1/b4 读取经 current_battle()/inventory() 成文出口
var _b1 = null # 当前战斗编排器(关卡结束置 null)
var _b4 = null # 关卡域共享 B4 容器(库存跨战斗延续)
# 协作对象私有(E1/E3 封装收口 2026-09-13):settlement/reward_economy 读取经 current_battle()/inventory() 成文出口
var _settlement = null # 当前战斗编排器(关卡结束置 null)
var _reward_economy = null # 关卡域共享 奖励道具经济 容器(库存跨战斗延续)
var hero_atk_pct := 0 # 测试面板覆盖值(切片专用)
var hero_hp_pct := 0
@@ -38,7 +38,7 @@ func _init(db_ref, rng_ref) -> void:
rng = rng_ref
# ---- 测试面板覆盖(模拟天赋树属性提升——仅测试用,正式侧归 C4;经 encounter 传入战斗)----
# ---- 测试面板覆盖(模拟天赋树属性提升——仅测试用,正式侧归 元进度;经 encounter 传入战斗)----
func set_hero_override(atk_pct: int, hp_pct: int) -> void:
hero_atk_pct = atk_pct
@@ -64,14 +64,14 @@ func restore_hero_base() -> void:
func start_level(level_index: int, seed_val: int) -> bool:
var row := _find_level_row(level_index)
if row.is_empty():
push_error("[C1] level_index %d 在 LevelTable 无行" % level_index)
push_error("[关卡节点地图] level_index %d 在 LevelTable 无行" % level_index)
return false
level_row = row
node_index = -1
level_result = ""
hero_hp = effective_max_hp()
_b4 = RewardEconomy.new(db, rng)
rng.new_adventure(seed_val) # 关卡起点 = 冒险起点(切片口径;跨关接续归 C2)
_reward_economy = RewardEconomy.new(db, rng)
rng.new_adventure(seed_val) # 关卡起点 = 冒险起点(切片口径;跨关接续归 冒险结构)
_running = true
return _enter_next_node()
@@ -99,15 +99,15 @@ func _enter_next_node() -> bool:
node_index += 1
var node: Dictionary = current_node()
if node.is_empty():
push_error("[C1] 节点链越界(末节点 Boss 应已结束关卡)")
push_error("[关卡节点地图] 节点链越界(末节点 Boss 应已结束关卡)")
return false
match String(node.get("type", "")):
"battle", "boss":
return _start_battle(node)
"reward":
var inst = _b4.grant_node_active(Vector2i(-1, -1), 0)
var inst = _reward_economy.grant_node_active(Vector2i(-1, -1), 0)
if not (inst is Dictionary):
push_error("[C1] 奖励点入库被拒")
push_error("[关卡节点地图] 奖励点入库被拒")
return false
node_entered.emit(node, {"snapshot": inst.snapshot})
return true
@@ -115,25 +115,25 @@ func _enter_next_node() -> bool:
var healed := _apply_rest()
node_entered.emit(node, {"healed": healed, "hp": hero_hp, "max_hp": effective_max_hp()})
return true
push_error("[C1] 未知节点类型: %s" % str(node))
push_error("[关卡节点地图] 未知节点类型: %s" % str(node))
return false
func _start_battle(node: Dictionary) -> bool:
_b1 = BattleOrchestrator.new(db, rng, _b4) # 共享 B4——库存跨战斗延续(关内延续裁决)
_settlement = BattleOrchestrator.new(db, rng, _reward_economy) # 共享 奖励道具经济——库存跨战斗延续(关内延续裁决)
# core↔core 接线只许方法 Callable(ADR-0001 静态规则 S6——lambda 捕获 self 会延长
# RefCounted 生命周期乃至成环:b1 的信号持捕获 runner 的 lambda,runner 又持 b1)
_b1.step_row.connect(_forward_step_row)
_b1.window_opened.connect(_forward_window_opened)
_b1.round_started.connect(_forward_round_started)
_b1.board_changed.connect(_forward_board_changed)
_b1.battle_finished.connect(_on_battle_finished)
# node_entered 先于 begin_battle:呈现层须先拿到 b1 引用,才能响应 begin 期间发出的
# RefCounted 生命周期乃至成环:settlement 的信号持捕获 runner 的 lambda,runner 又持 settlement)
_settlement.step_row.connect(_forward_step_row)
_settlement.window_opened.connect(_forward_window_opened)
_settlement.round_started.connect(_forward_round_started)
_settlement.board_changed.connect(_forward_board_changed)
_settlement.battle_finished.connect(_on_battle_finished)
# node_entered 先于 begin_battle:呈现层须先拿到 settlement 引用,才能响应 begin 期间发出的
# board_changed(首板安装)并重建棋盘——后发会导致首战棋盘不渲染(2026-09-11 playtest 缺陷①)
node_entered.emit(node, {})
if not _b1.begin_battle({"level_index": int(level_row.level_index), "node_depth": int(node.get("node_depth", 0)),
if not _settlement.begin_battle({"level_index": int(level_row.level_index), "node_depth": int(node.get("node_depth", 0)),
"hero_hp": hero_hp, "hero_atk": effective_atk(), "hero_max_hp": effective_max_hp()}):
push_error("[C1] 战斗启动失败(depth %s)" % str(node.get("node_depth", -1)))
push_error("[关卡节点地图] 战斗启动失败(depth %s)" % str(node.get("node_depth", -1)))
return false
return true
@@ -160,15 +160,15 @@ func _on_battle_finished(payload: Dictionary) -> void:
if result == "victory":
hero_hp = maxi(1, int(payload.get("hero_hp", 0))) # despawn 前捕获值(载荷自包含)
if String(node.get("type", "")) == "boss" or result == "defeat":
# Boss 胜利 = 关卡通关;任意战斗败北 = 关卡失败(Roguelike——冒险即终,重开归 C2/C4)
var left: int = _b1.level_clear()["remaining"] # 库存随关卡结束清空(关内延续裁决③)
if _b4.in_stock_count() != 0:
push_error("[C1] level_clear 后库存非空(清库违例——fail-fast)")
# Boss 胜利 = 关卡通关;任意战斗败北 = 关卡失败(Roguelike——冒险即终,重开归 冒险结构/元进度)
var left: int = _settlement.level_clear()["remaining"] # 库存随关卡结束清空(关内延续裁决③)
if _reward_economy.in_stock_count() != 0:
push_error("[关卡节点地图] level_clear 后库存非空(清库违例——fail-fast)")
return
level_result = "victory" if result == "victory" else "defeat"
var hp_left := hero_hp if result == "victory" else 0
_running = false
_b1 = null
_settlement = null
level_finished.emit({"result": level_result, "hp": hp_left, "node_index": node_index,
"node_type": String(node.get("type", "")), "remaining_items": left})
return
@@ -187,11 +187,11 @@ func _apply_rest() -> int:
# ---- 只读出口(E1/E3 封装收口:呈现层/工具经此读当前战斗与关卡库存,禁直读协作对象)----
func current_battle():
return _b1
return _settlement
func inventory() -> Array:
return _b4.query_inventory() if _b4 != null else []
return _reward_economy.query_inventory() if _reward_economy != null else []
# ---- 玩家输入转发(呈现层只与 runner 对话)----
@@ -205,11 +205,11 @@ func continue_after_node() -> bool:
func submit_window_selection(selection: Array) -> void:
if _b1 != null:
_b1.submit_window_selection(selection)
if _settlement != null:
_settlement.submit_window_selection(selection)
func player_request_refresh() -> bool:
if _b1 == null:
if _settlement == null:
return false
return _b1.player_request_refresh()
return _settlement.player_request_refresh()
@@ -1,20 +1,20 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
# Date: 2026-09-10
# A2 排雷玩法规则引擎:零随机/零表读取/零 UI——输入 = A1 产物 + 意图序列,输出 = 三事件流。
# 排雷玩法规则引擎:零随机/零表读取/零 UI——输入 = 棋盘生成 产物 + 意图序列,输出 = 三事件流。
# 核心契约:install 四不变式 · 初始展开 = R₀ 恰等 · 连锁不越旗 · 批次语义序仲裁(cleared > stop > 其余)。
# 实现 GDD:design/archive/gdd-2026-09-14/a2-minesweeper-rules.md(系统 ID A2——registry 追溯锚,命名规范见 CLAUDE.md)
# 实现 GDD:design/archive/gdd-2026-09-14/a2-minesweeper-rules.md(系统 ID A2·排雷玩法——registry 追溯锚,命名规范见 CLAUDE.md)
class_name MinesweeperRules
extends RefCounted
signal cell_revealed(evt: Dictionary)
signal flag_judged(evt: Dictionary)
signal round_end_board(payload: Dictionary) # 双发射源之棋盘路径(A2 外发;stopped 由 A3 直发)
signal round_end_board(payload: Dictionary) # 双发射源之棋盘路径(排雷玩法 外发;stopped 由 行动经济 直发)
const SUB_REASONS := ["not_adjacent", "target_revealed", "target_concealed", "flag_present", "flag_absent", "no_path", "out_of_catalog"]
var db
var a3
var action_economy
# 棋盘状态私有(E2 封装收口 2026-09-13):对外只暴露 current_board()/reveal_record() 等查询面,
# 禁止跨模块直读/直写原始字典——先例教训 = board_view 绕过查询面直读 revealed。
var _board: BoardData = null
@@ -24,9 +24,9 @@ var _hero := Vector2i.ZERO
var _spawn: Dictionary = {}
func _init(db_ref, a3_ref) -> void:
func _init(db_ref, action_economy_ref) -> void:
db = db_ref
a3 = a3_ref
action_economy = action_economy_ref
# ---- 板态 ----
@@ -35,7 +35,7 @@ func has_board() -> bool:
return _board != null
# 板产物只读出口(E2 收口——B1 快照 / 组合根 rebuild / 探针经此取板,禁再直读字段)
# 板产物只读出口(E2 收口——战斗结算 快照 / 组合根 rebuild / 探针经此取板,禁再直读字段)
func current_board() -> BoardData:
return _board
@@ -73,20 +73,20 @@ func clear_map_cleared() -> bool:
return safe_remaining() == 0
# ---- install(编排边界相位 = A3 Idle/Waiting;Active 拒绝——防轮内换板)----
# ---- install(编排边界相位 = 行动经济 Idle/Waiting;Active 拒绝——防轮内换板)----
# during_round = 轮内刷新编排路径(2026-09-12 方案 A,B1 唯一调用方):Active 相位合法——
# during_round = 轮内刷新编排路径(2026-09-12 方案 A,战斗结算 唯一调用方):Active 相位合法——
# 玩家挖掘中立即换图,棋盘状态(revealed/flags)随 install 重置,初始展开事件照发(initial 通道)
func install_board(b: BoardData, during_round := false) -> Dictionary:
if a3.phase == a3.PHASE_ACTIVE and not during_round:
if action_economy.phase == action_economy.PHASE_ACTIVE and not during_round:
return _outcome(false, "rejected", "phase", "")
if a3.phase == a3.PHASE_IDLE and during_round:
if action_economy.phase == action_economy.PHASE_IDLE and during_round:
return _outcome(false, "rejected", "phase", "")
if b == null:
return _outcome(false, "rejected", "board_invalid", "out_of_catalog")
var inv := _install_invariant_faults(b)
if not inv.is_empty():
push_error("[A2] install 不变式违例: %s" % str(inv))
push_error("[排雷玩法] install 不变式违例: %s" % str(inv))
return _outcome(false, "rejected", "board_invalid", inv[0])
_board = b
_revealed.clear()
@@ -113,7 +113,7 @@ func flag_count() -> int:
func retire_board(during_round := false) -> Dictionary:
if _board == null:
return _outcome(true, "executed", "", "") # 幂等受理
if a3.phase == a3.PHASE_ACTIVE and not during_round:
if action_economy.phase == action_economy.PHASE_ACTIVE and not during_round:
return _outcome(false, "rejected", "phase", "")
_board = null
_revealed.clear()
@@ -148,7 +148,7 @@ func _install_invariant_faults(b: BoardData) -> Array:
return faults
# 初始展开规范序:触发格 BFS、8 邻行主序(≡ A1 F1 R₀ 算子;禁外扩环、禁只开 3×3)
# 初始展开规范序:触发格 BFS、8 邻行主序(≡ 棋盘生成 F1 R₀ 算子;禁外扩环、禁只开 3×3)
func _initial_expansion_order(b: BoardData) -> Array:
var order: Array = b.spawn_cells().duplicate()
var seen := {}
@@ -172,7 +172,7 @@ func settle_batch(intents: Array) -> Array:
var outcomes: Array = []
if intents.is_empty():
return outcomes
if a3.phase != a3.PHASE_ACTIVE or _board == null:
if action_economy.phase != action_economy.PHASE_ACTIVE or _board == null:
for intent in intents:
outcomes.append(_outcome(false, "rejected", "phase", ""))
return outcomes
@@ -191,7 +191,7 @@ func settle_batch(intents: Array) -> Array:
continue
var res := _execute(item.intent)
outcomes.append(res)
if a3.phase != a3.PHASE_ACTIVE:
if action_economy.phase != action_economy.PHASE_ACTIVE:
round_over = true
return outcomes
@@ -202,7 +202,7 @@ func _classify(intent: Dictionary) -> int:
var c: Vector2i = intent.get("cell", Vector2i(-9, -9))
if _board_face_dig(c) == "" and not _board.is_mine(c):
# 模拟 Chain(O, c) 后清图判定
var chain := _chain_closure(c, {})
var chain := _chain_closure(c, _flags) # 与真实执行同口径——空旗集捷径会在旗挡连锁时误判 C_dig(排雷玩法:真模拟)
var remaining := 0
for sc in _board.safe_cells():
if not _revealed.has(sc) and not chain.has(sc):
@@ -227,7 +227,7 @@ func _execute(intent: Dictionary) -> Dictionary:
return _do_move(intent.get("cell", Vector2i(-9, -9)))
"stop":
return _do_stop()
push_warning("[A2] 目录外意图: %s" % str(intent))
push_warning("[排雷玩法] 目录外意图: %s" % str(intent))
return _outcome(false, "rejected", "board_face", "out_of_catalog")
@@ -245,8 +245,8 @@ func _do_dig(c: Vector2i) -> Dictionary:
for cc in chain.keys():
if cc != c and not _revealed.has(cc):
chain_size += 1
# 资源面/相位 + cleared 仲裁在 A3 事务内原子完成
if not a3.request_dig(chain_size, clears):
# 资源面/相位 + cleared 仲裁在 行动经济 事务内原子完成
if not action_economy.request_dig(chain_size, clears):
return _outcome(false, "rejected", "resource_face", "")
# 事务内执行:首格 + 连锁新格揭示(BFS 规范序,含链开雾格——零特判);事件 = O′ ∖ O
var reveal_order: Array = _chain_reveal_order(c, _flags)
@@ -267,7 +267,7 @@ func _do_dig(c: Vector2i) -> Dictionary:
"cell": rc, "kind": "safe", "digit": _board.digit_of(rc),
"variant_ids": _board.variant_ids(rc), "channel": channel, "reward_eligible": true,
})
if a3.phase != a3.PHASE_ACTIVE:
if action_economy.phase != action_economy.PHASE_ACTIVE:
_emit_round_end_flags_and_payload()
return _outcome(true, "executed", "", "")
@@ -335,12 +335,12 @@ func _do_flag_place(c: Vector2i) -> Dictionary:
sub = "flag_present"
if sub != "":
return _outcome(false, "rejected", "board_face", sub)
if not a3.try_spend("flag_place"):
if not action_economy.try_spend("flag_place"):
return _outcome(false, "rejected", "resource_face", "")
_flags[c] = true # 旗动作零格子事件外发(发射面封闭;反馈经调用返回值命令式驱动)
if a3.phase != a3.PHASE_ACTIVE:
# 旗动作花完末点 AP 同样终结轮次(A3 三出口——AP 用完与动作种类无关;缺漏使 round_end
# 永不发出、B1 的 Waiting 结算点永不到达 = 整局软锁。playtest 缺陷②,行为探针复现)
if action_economy.phase != action_economy.PHASE_ACTIVE:
# 旗动作花完末点 AP 同样终结轮次(行动经济 三出口——AP 用完与动作种类无关;缺漏使 round_end
# 永不发出、战斗结算 的 Waiting 结算点永不到达 = 整局软锁。playtest 缺陷②,行为探针复现)
_emit_round_end_flags_and_payload()
return _outcome(true, "executed", "", "")
@@ -355,10 +355,10 @@ func _do_flag_pull(c: Vector2i) -> Dictionary:
sub = "flag_absent"
if sub != "":
return _outcome(false, "rejected", "board_face", sub)
if not a3.try_spend("flag_pull"):
if not action_economy.try_spend("flag_pull"):
return _outcome(false, "rejected", "resource_face", "")
_flags.erase(c)
if a3.phase != a3.PHASE_ACTIVE:
if action_economy.phase != action_economy.PHASE_ACTIVE:
_emit_round_end_flags_and_payload() # 同 flag_place——末点 AP 轮末发射(playtest 缺陷②)
return _outcome(true, "executed", "", "")
@@ -384,7 +384,7 @@ func compute_move_path(target: Vector2i) -> Array:
return []
if target == _hero:
return []
# BFS(8 邻行主序)——路径 = 合法性载体 + D1 预览数据源
# BFS(8 邻行主序)——路径 = 合法性载体 + HUD框架预览数据源
var prev := {_hero: _hero}
var queue: Array = [_hero]
while not queue.is_empty():
@@ -405,20 +405,20 @@ func compute_move_path(target: Vector2i) -> Array:
return path
# ---- 停手(经批次入口委托 A3;先发旗判定批再委托——事件序契约 flag_judged < round_end 全路径统一)----
# ---- 停手(经批次入口委托 行动经济;先发旗判定批再委托——事件序契约 flag_judged < round_end 全路径统一)----
func _do_stop() -> Dictionary:
_emit_flags_batch() # 基线 = 事务开始旗位(停手不改旗位)
if not a3.request_stop():
if not action_economy.request_stop():
return _outcome(false, "rejected", "phase", "")
# A3 直发 stopped 路径 round_end;A2 禁双发(双发射源纪律)
# 行动经济 直发 stopped 路径 round_end;排雷玩法 禁双发(双发射源纪律)
return _outcome(true, "executed", "", "")
# ---- 轮末旗判定 + 棋盘路径 round_end 外发 ----
func _emit_flags_batch() -> void:
# 判定基线 = 轮次结束事务开始时旗位;行主序枚举(确定性);被拔的不判;诚实重复上报(去重归 B1)
# 判定基线 = 轮次结束事务开始时旗位;行主序枚举(确定性);被拔的不判;诚实重复上报(去重归 战斗结算)
var cells: Array = _flags.keys()
cells.sort_custom(func(a, b): return a.y * 1000 + a.x < b.y * 1000 + b.x)
for c in cells:
@@ -427,7 +427,7 @@ func _emit_flags_batch() -> void:
func _emit_round_end_flags_and_payload() -> void:
_emit_flags_batch()
var payload: Dictionary = a3.last_round_end # 逐字段取自 A3(读取权 = 接口契约)
var payload: Dictionary = action_economy.last_round_end # 逐字段取自 行动经济(读取权 = 接口契约)
round_end_board.emit({"path_tag": payload.get("path_tag", ""), "continuation_hint": payload.get("continuation_hint", "")})
@@ -1,14 +1,14 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 可选类供给能否按"格极稀 / 板≈0.78 / 战斗期望≈1(首件保证 + 指数递减)"三尺度被精确控制
# Date: 2026-09-10
# B4 奖励与道具经济:install 期身份预分配(roll_board 恰一次、消费 reward_roll 封闭流恰一 scope 批)·
# 奖励道具经济:install 期身份预分配(roll_board 恰一次、消费 reward_roll 封闭流恰一 scope 批)·
# 授予期零随机 · 库存关卡域(battle_over 全清)· 道具伤害恒定表值 · Ready 后只读。
# 2026-09-11 可选类供给控制器批(用户拍板):格子池只剩即时类;可选类经 RewardSupplyController 注入
# (战斗首件保证 + p0×decay^(j-1) 指数递减);旧 F5 格池刀保底(k_dmg)休眠;清图槽改一次性伤害。
# 2026-09-14 伤害量值区间批(用户拍板②):区间行(power_min/max 可选配对)在身份预分配批内追加取号
# (④ 步,既有取号序零扰动);快照 E14 口径不变——快照 power = roll 值,B1/呈现零改动;授予期零随机不变。
# (④ 步,既有取号序零扰动);快照 E14 口径不变——快照 power = roll 值,战斗结算/呈现零改动;授予期零随机不变。
# 切片登记:取号约定 = randi_range(0, W-1)(实现期 ADR 事实,切片内恒定)。
# 实现 GDD:design/archive/gdd-2026-09-14/b4-reward-item-economy.md(系统 ID B4——registry 追溯锚,命名规范见 CLAUDE.md)
# 实现 GDD:design/archive/gdd-2026-09-14/b4-reward-item-economy.md(系统 ID B4·奖励道具经济——registry 追溯锚,命名规范见 CLAUDE.md)
class_name RewardEconomy
extends RefCounted
@@ -34,7 +34,7 @@ func _init(db_ref, rng_ref) -> void:
_supply = RewardSupplyController.new(db.reward_config)
# ---- 战斗开始(B1.begin_battle 编排调用——B4 唯一调用方纪律不变)----
# ---- 战斗开始(战斗结算.begin_battle 编排调用——奖励道具经济 唯一调用方纪律不变)----
# 供给控制器战斗域计数清零;库存(关卡域)不受影响——关内延续照旧
func notify_battle_start() -> void:
_supply.notify_battle_start()
@@ -45,18 +45,18 @@ func notify_battle_start() -> void:
func roll_board(eligible: Array) -> bool:
# 相位守卫:Ready 再调 = double-roll 拒绝(E5);Retired(换图后)= 合法新批重置(E6 roll 半)
if _phase == "ready":
push_warning("[B4] roll_board 相位守卫拒绝(E5——double-roll 编排违约显式暴露)")
push_warning("[奖励道具经济] roll_board 相位守卫拒绝(E5——double-roll 编排违约显式暴露)")
return false
if eligible.is_empty():
_fatal("roll_board 空资格清单(E1 兜底 fail-fast——A2 不变式④前置)")
_fatal("roll_board 空资格清单(E1 兜底 fail-fast——排雷玩法 不变式④前置)")
return false
# 行主序规范化 + 去重(排序责任不外推给 B1)
# 行主序规范化 + 去重(排序责任不外推给 战斗结算)
var seen := {}
var list: Array = []
for e in eligible:
var c: Vector2i = e.cell
if seen.has(c):
push_warning("[B4] 资格清单重复格已去重: %s" % str(c))
push_warning("[奖励道具经济] 资格清单重复格已去重: %s" % str(c))
continue
seen[c] = true
list.append({"cell": c, "fog": bool(e.get("fog", false))})
@@ -86,7 +86,7 @@ func roll_board(eligible: Array) -> bool:
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 口径,B1/呈现零改动
# 授予期零随机纪律不变——快照 E14 口径,战斗结算/呈现零改动
for cell in draft.keys():
var row: Dictionary = _row_by_id(draft[cell])
if row.has("power_min"):
@@ -100,7 +100,7 @@ func roll_board(eligible: Array) -> bool:
return true
# ---- 关卡节点奖励(2026-09-11 关卡扩展批——线性奖励点,正式归属 B4/C1 修订)----
# ---- 关卡节点奖励(2026-09-11 关卡扩展批——线性奖励点,正式归属 奖励道具经济/关卡节点地图 修订)----
# 奖励点 = 用户拍板的关卡节点类型:在本表全部可选类活行(damage_item / heal_pack,即"主动道具")
# 中 roll 一件并直接入库。消耗 reward_roll 恰一 scope(与 board install 批同流异 scope——流集合封闭不变)。
# cell=(-1,-1)/round_index=0 = 非格子来源标记(关卡节点授予,区别于安全格拾取的呈现面)。
@@ -139,7 +139,7 @@ func query_identity(cell: Vector2i):
# 身份完整快照(轮内刷新批次隔离用——facts 缓冲期捕获,授予期 mapping 可能已换批):
# 返回与 grant 相同结构的快照(display_name/power/effect_id 齐备),供 B1 冻结授予直通
# 返回与 grant 相同结构的快照(display_name/power/effect_id 齐备),供 战斗结算 冻结授予直通
func query_full_snapshot(cell: Vector2i):
if _phase != "ready":
_diag("query_full_snapshot 非 Ready 相位")
@@ -150,7 +150,7 @@ func query_full_snapshot(cell: Vector2i):
return _snapshot(_row_by_id(rid), int(_power_roll.get(cell, -1)))
# ---- 受理面(B1 编排期调用;E6 收口 2026-09-13:失败统一返回 null——此前 false/null 混用)----
# ---- 受理面(战斗结算 编排期调用;E6 收口 2026-09-13:失败统一返回 null——此前 false/null 混用)----
func grant(cell: Vector2i):
if _phase != "ready":
@@ -160,7 +160,7 @@ func grant(cell: Vector2i):
_diag("grant 清单外坐标(E19)")
return null
if _granted.has(cell):
_diag("grant 同格重复受理(E18 幂等兜底——受理恰一次归 B1)")
_diag("grant 同格重复受理(E18 幂等兜底——受理恰一次归 战斗结算)")
return _granted[cell]
var row: Dictionary = _row_by_id(_mapping[cell])
var snap := _snapshot(row, int(_power_roll.get(cell, -1)))
@@ -202,7 +202,7 @@ func stock(snapshot: Dictionary, source_cell: Vector2i, round_index: int):
return inst
# 窗口选用消耗:零游戏态守卫(照常消耗——E10;满血/零效果守卫归 B1)
# 窗口选用消耗:零游戏态守卫(照常消耗——E10;满血/零效果守卫归 战斗结算)
func use(index: int):
if _inventory_cleared:
_diag("use 战斗已清库")
@@ -275,7 +275,7 @@ func retire_mapping() -> bool:
return true
# ---- F6 当量供给(B1 条款①对照实物输入)----
# ---- F6 当量供给(战斗结算 条款①对照实物输入)----
func equivalence_supply() -> Dictionary:
var live: Array = []
@@ -339,9 +339,9 @@ func _snapshot(row: Dictionary, rolled_power: int = -1) -> Dictionary:
func _diag(msg: String) -> void:
push_warning("[B4] " + msg)
push_warning("[奖励道具经济] " + msg)
func _fatal(msg: String) -> void:
push_error("[B4/fatal] " + msg)
push_error("[奖励道具经济/fatal] " + msg)
assert(false, msg)
@@ -1,12 +1,12 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 可选类供给能否按"格极稀 <0.01 / 板≈0.78 / 战斗期望≈1(首件保证 + 指数递减)"三尺度被精确控制
# Date: 2026-09-11
# 可选类供给控制器(2026-09-11 用户拍板批——B4 私有组合件):
# 可选类供给控制器(2026-09-11 用户拍板批——奖励道具经济 私有组合件):
# 格子权重池不再产出可选类;本控制器在 install 期决定"本板是否注入 1 件可选类"——
# 战斗内尚未获取首件 → 本板必注入(首件保证);已获取 j 件 → 概率 p0×decay^(j-1)(指数下降、永不绝育)。
# 行值快照(p0/decay 构造期快照——抗热重载,B2 同款);零自有随机数(判定取号经 B4 传入的 reward_roll
# 封闭流实例——Rule 5);"获取"以实际发放入库为准(预分配未挖到不计数——授予侧经 B4.grant 挂钩上报)。
# 实现 GDD:design/archive/gdd-2026-09-14/b4-reward-item-economy.md(系统 ID B4——registry 追溯锚,命名规范见 CLAUDE.md)
# 行值快照(p0/decay 构造期快照——抗热重载,敌人系统 同款);零自有随机数(判定取号经 奖励道具经济 传入的 reward_roll
# 封闭流实例——Rule 5);"获取"以实际发放入库为准(预分配未挖到不计数——授予侧经 奖励道具经济.grant 挂钩上报)。
# 实现 GDD:design/archive/gdd-2026-09-14/b4-reward-item-economy.md(系统 ID B4·奖励道具经济——registry 追溯锚,命名规范见 CLAUDE.md)
class_name RewardSupplyController
extends RefCounted
@@ -20,12 +20,12 @@ func _init(reward_config) -> void:
_decay = clampf(float(reward_config.active_decay), 0.001, 1.0)
# B1.begin_battle 编排调用(B4 转发)——战斗域计数重置;跨战斗库存延续不受影响
# 战斗结算.begin_battle 编排调用(奖励道具经济 转发)——战斗域计数重置;跨战斗库存延续不受影响
func notify_battle_start() -> void:
_granted = 0
# install 期注入判定(B4.roll_board 在身份批 + 清图槽之后调用;取号 = 传入封闭流恰一次)
# install 期注入判定(奖励道具经济.roll_board 在身份批 + 清图槽之后调用;取号 = 传入封闭流恰一次)
func should_inject(stream: RandomNumberGenerator) -> bool:
if _granted == 0:
return true # 首件保证
@@ -33,7 +33,7 @@ func should_inject(stream: RandomNumberGenerator) -> bool:
return stream.randf() < p
# B4.grant 实际发放可选类时上报(发放即获取;满血拒"使用"不影响——那是消耗侧语义)
# 奖励道具经济.grant 实际发放可选类时上报(发放即获取;满血拒"使用"不影响——那是消耗侧语义)
func notify_granted() -> void:
_granted += 1
@@ -1,24 +1,24 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
# Date: 2026-09-10
# D2 RngService autoload——封闭命名流单点(Rule 5)。玩法代码禁止 randomize()/自建 RandomNumberGenerator。
# 实现 GDD:design/archive/gdd-2026-09-14/d2-data-config.md(系统 ID D2——registry 追溯锚,命名规范见 CLAUDE.md)
# RngService autoload(数据配置)——封闭命名流单点(Rule 5)。玩法代码禁止 randomize()/自建 RandomNumberGenerator。
# 实现 GDD:design/archive/gdd-2026-09-14/d2-data-config.md(系统 ID D2·数据配置——registry 追溯锚,命名规范见 CLAUDE.md)
# R2 派生公式(黄金向量已预验证):derive = (SALT XOR wrap64(master + scope×GOLDEN)) AND 0x7FFF…
# 取号语义:全新实例首个 open_stream = scope 0(B4 预推进术语的登记口径)。
# 取号语义:全新实例首个 open_stream = scope 0(奖励道具经济 预推进术语的登记口径)。
extends Node
const STREAM_NAMES := ["board_gen", "reward_roll", "encounter"] # 2026-09-12 增 encounter(遭遇随机化批,用户拍板——registry rng_service 条目同步)
# 分段表达式书写(D2 R2 实现级规约——禁 ≥2^63 完整字面量)
# 分段表达式书写(数据配置 R2 实现级规约——禁 ≥2^63 完整字面量)
const GOLDEN_RATIO := (0x9E3779B9 << 32) | 0x7F4A7C15 # 0x9E3779B97F4A7C15
const SALT_BOARD_GEN := (0xA0761D64 << 32) | 0x78BD642F # 0xA0761D6478BD642F
const SALT_REWARD_ROLL := (0xE7037ED1 << 32) | 0xA0B428DB # 0xE7037ED1A0B428DB
const SALT_ENCOUNTER := (0x5DEECE66D << 32) | 0x2545F491 # 0x5DEECE66D2545F491(遭遇骰——2026-09-12)
const SALT_ENCOUNTER := (0x8EBC6AF0 << 32) | 0x9C88C6E3 # 0x8EBC6AF09C88C6E3(遭遇骰——registry stream_seed_derivation 权威值;旧值 0x5DEECE66D<<32 超 int64 被截断,黄金向量漂移)
const OUT_MASK := 0x7FFFFFFFFFFFFFFF
const INT64_MIN := -9223372036854775808
# master_seed 只读出口(E5 封装收口 2026-09-13):写入只经 set_master_seed(计数器全零断言
# 不可绕过);外部读 master_seed 供 D3 快照契约观测。
# 不可绕过);外部读 master_seed 供 存档序列化 快照契约观测。
var _master_seed: int = -1 # -1 = 未播种(取数即致命,EC7)
var master_seed: int:
get: return _master_seed
@@ -60,7 +60,7 @@ func set_master_seed(seed_val: int) -> void:
_counters["reward_roll"] = 0
# 切片登记:冒险开始重播种入口(正式侧调用权归 C2;切片主菜单 = 冒险起点)
# 切片登记:冒险开始重播种入口(正式侧调用权归 冒险结构;切片主菜单 = 冒险起点)
func new_adventure(seed_val: int) -> void:
_counters["board_gen"] = 0
_counters["reward_roll"] = 0
@@ -74,7 +74,7 @@ func open_stream(stream: String) -> RandomNumberGenerator:
push_error("[RngService] 未知名流: %s" % stream)
assert(false, "未知名流(EC12)")
if _master_seed < 0:
push_error("[RngService] 未设 master_seed 即取数(EC7——C2 未接线的显式暴露)")
push_error("[RngService] 未设 master_seed 即取数(EC7——冒险结构 未接线的显式暴露)")
assert(false, "未播种流取数致命(EC7)")
var scope: int = _counters[stream]
_counters[stream] = scope + 1
@@ -83,6 +83,6 @@ func open_stream(stream: String) -> RandomNumberGenerator:
return rg
# 只读计数器(D3 快照契约的观测面)
# 只读计数器(存档序列化 快照契约的观测面)
func counter(stream: String) -> int:
return _counters[stream]
@@ -1,10 +1,10 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
# Date: 2026-09-10
# B3 统一状态容器:三向效果同构实例 · 单一时限量纲 remaining_settlements(先兑付后衰减)·
# 增益状态·统一状态容器:三向效果同构实例 · 单一时限量纲 remaining_settlements(先兑付后衰减)·
# 按行叠层(stack 授予不触碰 R)· 授予时数值快照抗热重载 · 零随机零信号。
# 注:MVP 单敌假设经 B2 回验不成立(定多敌)——enemy 侧 side-wide 语义按 registry 改写执行。
# 实现 GDD:design/archive/gdd-2026-09-14/b3-status-effect.md(系统 ID B3——registry 追溯锚,命名规范见 CLAUDE.md)
# 注:MVP 单敌假设经 敌人系统 回验不成立(定多敌)——enemy 侧 side-wide 语义按 registry 改写执行。
# 实现 GDD:design/archive/gdd-2026-09-14/b3-status-effect.md(系统 ID B3·增益状态——registry 追溯锚,命名规范见 CLAUDE.md)
class_name StatusContainer
extends RefCounted
@@ -19,21 +19,21 @@ func _init(db_ref) -> void:
func notify_battle_start() -> bool:
if _open:
push_warning("[B3] Active 下重复 notify_battle_start(EC11)")
push_warning("[增益状态] Active 下重复 notify_battle_start(EC11)")
return false
_open = true
_instances.clear()
return true
# 唯一授予入口(挖掘期零直写——B1 结算编排期统一调用)
# 唯一授予入口(挖掘期零直写——战斗结算 结算编排期统一调用)
func apply_effect(effect_id: String, source: Dictionary) -> bool:
if not _open:
push_warning("[B3] 域闭授予(EC1)")
push_warning("[增益状态] 域闭授予(EC1)")
return false
var row: Dictionary = db.status_effect_row(effect_id)
if row.is_empty():
push_warning("[B3] 未知 effect_id(EC2): %s" % effect_id)
push_warning("[增益状态] 未知 effect_id(EC2): %s" % effect_id)
return false
var policy: String = row.stack_policy
var dur: int = int(row.duration_settlements)
@@ -57,12 +57,12 @@ func apply_effect(effect_id: String, source: Dictionary) -> bool:
for inst in _instances:
if inst.effect_id == effect_id:
if inst.layers >= max_stack:
return false # 满层拒绝原子——容器零写入(溢出行归 B1)
return false # 满层拒绝原子——容器零写入(溢出行归 战斗结算)
inst.layers += 1 # stack 授予不触碰 R(spec 决议)
return true
_instances.append(_new_instance(effect_id, 1, dur, source, snap_m, snap_unit, snap_display))
return true
push_warning("[B3] 未知 stack_policy")
push_warning("[增益状态] 未知 stack_policy")
return false
@@ -99,12 +99,12 @@ func query_active(side: String) -> Array:
"display_name": inst.snap_display,
"snap_magnitude": inst.snap_m,
"snap_unit": inst.snap_unit,
"effective_value": inst.layers * inst.snap_m, # L×m 加算(percent 越域不 clamp——收口归 B1)
"effective_value": inst.layers * inst.snap_m, # L×m 加算(percent 越域不 clamp——收口归 战斗结算)
})
return out
# 池层数读值(B1 战斗段伏击池 L 的消费面)
# 池层数读值(战斗结算·战斗段伏击池 L 的消费面)
func pool_layers(effect_id: String) -> int:
for inst in _instances:
if inst.effect_id == effect_id:
@@ -112,7 +112,7 @@ func pool_layers(effect_id: String) -> int:
return 0
# 轮末衰减:先兑付后衰减;每实例恰 −1;归 0 移除;返回移除数 k(恰一次执行由 B1 编排信任承载)
# 轮末衰减:先兑付后衰减;每实例恰 −1;归 0 移除;返回移除数 k(恰一次执行由 战斗结算 编排信任承载)
func decay_round() -> int:
if not _open:
return 0 # EC13 域闭兜底读法——容器恒空,无独立分支不诊断
@@ -130,7 +130,7 @@ func decay_round() -> int:
func notify_battle_end() -> bool:
if not _open:
push_warning("[B3] BattleClosed 下重复 notify_battle_end(EC11)")
push_warning("[增益状态] BattleClosed 下重复 notify_battle_end(EC11)")
return false
_open = false
_instances.clear() # 全清不跨战斗
@@ -1,11 +1,11 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
# Date: 2026-09-10
# B2 战斗单位系统:hero/enemy 统一受伤死亡接口 · 行为环确定性轮转(死亡即不动手/先作用后推进)·
# 行值快照抗热重载 · side-wide 伏击池读值由调用方注入(B2 无池写面)· 编排被动零事件。
# 切片登记接口漂移:heal() 为切片新增(B1 治疗 clamp 需要写入口;B2 API 封闭枚举原本无治疗面——
# 正式侧应走 B2 GDD 修订,切片以头注登记)。
# 实现 GDD:design/archive/gdd-2026-09-14/b2-enemy-system.md(系统 ID B2——registry 追溯锚,命名规范见 CLAUDE.md)
# 敌人系统·战斗单位容器:hero/enemy 统一受伤死亡接口 · 行为环确定性轮转(死亡即不动手/先作用后推进)·
# 行值快照抗热重载 · side-wide 伏击池读值由调用方注入(敌人系统 无池写面)· 编排被动零事件。
# 切片登记接口漂移:heal() 为切片新增(战斗结算 治疗 clamp 需要写入口;敌人系统 API 封闭枚举原本无治疗面——
# 正式侧应走 敌人系统 GDD 修订,切片以头注登记)。
# 实现 GDD:design/archive/gdd-2026-09-14/b2-enemy-system.md(系统 ID B2·敌人系统——registry 追溯锚,命名规范见 CLAUDE.md)
class_name UnitContainer
extends RefCounted
@@ -25,35 +25,35 @@ func _init(db_ref) -> void:
# E4 收口(2026-09-13):加成值经参数显式传入(atk_override/max_hp_override,-1 = 表值兜底)——
# 此前调用方写 db.hero 表值再由本类读表 = 共享可变单例传参(双写叠加 6→18→54 教训根源)。
# 接口漂移登记:spawn 签名 +2 参数(B2 GDD API 封闭枚举——正式侧 GDD/registry 修订批随 ADR-0009)。
# enemy_atk_override(2026-09-14 敌方攻击区间批):B1 遭遇骰同流的 spawn 期整战持久 roll 结果
# (-1 = 表值定值路径;≥0 = 行值替换后再乘关卡 atk_mult——修饰语义不变)。B2 零随机纪律保持。
# 接口漂移登记:spawn 签名 +2 参数(敌人系统 GDD API 封闭枚举——正式侧 GDD/registry 修订批随 ADR-0009)。
# enemy_atk_override(2026-09-14 敌方攻击区间批):战斗结算 遭遇骰同流的 spawn 期整战持久 roll 结果
# (-1 = 表值定值路径;≥0 = 行值替换后再乘关卡 atk_mult——修饰语义不变)。敌人系统 零随机纪律保持。
func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_override: int = -1,
atk_override: int = -1, max_hp_override: int = -1, enemy_atk_override: int = -1) -> bool:
if _spawned:
push_warning("[B2] Alive 相位重复 spawn(相位守卫)")
push_warning("[敌人系统] Alive 相位重复 spawn(相位守卫)")
return false
var row: Dictionary = db.resolve_enemy_rowset(level_index, node_depth)
if row.is_empty():
push_error("[B2] 遭遇行解析失败/越域 (%d,%d)" % [level_index, node_depth])
push_error("[敌人系统] 遭遇行解析失败/越域 (%d,%d)" % [level_index, node_depth])
assert(false, "遭遇行悬空(R1 前置守卫)")
return false
var eff_atk: int = atk_override if atk_override >= 1 else int(db.hero.atk)
var eff_max_hp: int = max_hp_override if max_hp_override >= 1 else int(db.hero.max_hp)
if hero_hp > eff_max_hp or hero_hp <= 0:
push_warning("[B2] hero_hp 入参越界(EC16): %d" % hero_hp)
push_warning("[敌人系统] hero_hp 入参越界(EC16): %d" % hero_hp)
return false
# max_hp = 生效上限(非入参 hero_hp)——2026-09-11 关卡扩展批修复:跨战斗携带 HP 时上限不得压扁
# (恢复点/治疗包的满血守卫与治疗上限随之失效——切片登记接口语义澄清)
_hero = {"hp": hero_hp, "max_hp": eff_max_hp, "atk": eff_atk, "is_hero": true, "alive": true, "display_name": "探险者", "steps": 0, "pattern": [], "enemy_id": "hero"}
_enemies.clear() # C3 DifficultyTable 修饰(2026-09-12 结构位兑现):施加于行值、行值快照之前——MVP 空表 = 恒 1.0 无行为(AC-10)
_enemies.clear() # 难度曲线与Boss DifficultyTable 修饰(2026-09-12 结构位兑现):施加于行值、行值快照之前——MVP 空表 = 恒 1.0 无行为(AC-10)
var mod: Dictionary = db.resolve_difficulty_modifiers(level_index, node_depth)
var hp_mult: float = float(mod.get("hp_mult", 1.0))
var atk_mult: float = float(mod.get("atk_mult", 1.0))
# unit_count_override ≥1 = B1 遭遇骰结果(B2 零随机纪律——收到即确定值;-1 = 行值定值路径)
# unit_count_override ≥1 = 战斗结算 遭遇骰结果(敌人系统 零随机纪律——收到即确定值;-1 = 行值定值路径)
var count: int = unit_count_override if unit_count_override >= 1 else int(row.unit_count)
if count < 1 or count > 3:
push_warning("[B2] 实例化数量越域: %d" % count)
push_warning("[敌人系统] 实例化数量越域: %d" % count)
return false
for i in range(count):
var eff_row_atk: int = enemy_atk_override if enemy_atk_override >= 0 else int(row.atk)
@@ -67,7 +67,7 @@ func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_override:
"heavy_multiplier": int(row.heavy_multiplier),
"pattern": Array(row.pattern), # 行值快照(拷贝)
"advantage_per_round_cap": int(row.advantage_per_round_cap),
"armor_type": String(row.get("armor_type", "none")), # 护甲类型快照(克制矩阵消费面——B1 战斗段)
"armor_type": String(row.get("armor_type", "none")), # 护甲类型快照(克制矩阵消费面——战斗结算·战斗段)
"steps": 0,
"alive": true,
"is_hero": false,
@@ -126,11 +126,11 @@ func apply_damage(u: Dictionary, n: int) -> bool:
return false
u.hp = maxi(0, u.hp - n)
if u.hp == 0:
u.alive = false # Dead 不可逆(死亡判定时机归 B1)
u.alive = false # Dead 不可逆(死亡判定时机归 战斗结算)
return true
# 治疗(切片登记接口漂移——B1 治疗 clamp 的写入口):返回实际回复量
# 治疗(切片登记接口漂移——战斗结算 治疗 clamp 的写入口):返回实际回复量
func heal(u: Dictionary, n: int) -> int:
if n < 0 or u.is_empty() or _despawned or not u.alive:
_diag("heal 非法/失效句柄")
@@ -174,7 +174,7 @@ func all_enemies() -> Array:
return _enemies.duplicate()
# F3 机制算术纯函数(B1 预截流消费面):cap 判定在授予前;守恒 granted + overflow = mines_r
# F3 机制算术纯函数(战斗结算 预截流消费面):cap 判定在授予前;守恒 granted + overflow = mines_r
func ambush_grant_plan(mines_r: int, cap: int) -> Dictionary:
var granted: int = mini(mines_r, cap)
return {"granted": granted, "overflow": mines_r - granted}
@@ -191,4 +191,4 @@ func despawn_all() -> bool:
func _diag(msg: String) -> void:
push_warning("[B2] " + msg)
push_warning("[敌人系统] " + msg)
@@ -1,7 +1,7 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
# Date: 2026-09-10
# D2 ActionEconomyTable schema(A3 回填值,单行全局;Rule 4 豁免)。
# 数据配置 ActionEconomyTable schema(行动经济 回填值,单行全局;Rule 4 豁免)。
# 数值域与跨字段校验依据 design/archive/gdd-2026-09-14/a3-action-economy.md "ActionEconomyTable Schema 定稿"。
class_name ActionEconomyTable
extends Resource
@@ -34,7 +34,7 @@ func validate() -> Array:
errs.append({"category": "frozen", "message": "move_ap_cost 结构冻结为 0: %d" % move_ap_cost})
if chain_ap_cost != 0:
errs.append({"category": "frozen", "message": "chain_ap_cost 结构冻结为 0: %d" % chain_ap_cost})
# 跨字段(A3 三轮评审增补):任一动作成本 ≤ ap_per_round,违规加载期致命
# 跨字段(行动经济 三轮评审增补):任一动作成本 ≤ ap_per_round,违规加载期致命
for pair in [["dig_ap_cost", dig_ap_cost], ["flag_place_ap_cost", flag_place_ap_cost], ["flag_pull_ap_cost", flag_pull_ap_cost]]:
if pair[1] > ap_per_round:
errs.append({"category": "cross_field", "message": "%s(%d) > ap_per_round(%d)——挖掘/旗动作永久不可用软锁" % [pair[0], pair[1], ap_per_round]})
@@ -1,16 +1,16 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
# Date: 2026-09-10
# D2 BoardGenTable schema(A1 回填值;键 (level_index, node_depth)——首个进度键表)。
# 数据配置 BoardGenTable schema(棋盘生成 回填值;键 (level_index, node_depth)——首个进度键表)。
# 依据 design/archive/gdd-2026-09-14/a1-board-generation.md "BoardGenTable Schema 定稿" + F2/F2b/F2c 谓词。
class_name BoardGenTable
extends Resource
@export var rows: Array = [] # Array[Dictionary]
const SOLVER_RULES := ["single", "single_subset"]
const FOG_RATIO_ABS_CAP := 0.25 # B1 修订——雾占比绝对上限
const FOG_RATIO_ABS_CAP := 0.25 # 战斗结算 修订——雾占比绝对上限
const COST_BUDGET := 16384 # F2c provisional 量级护栏(禁墙钟逃生)
const TARGET_ABS_FLOOR := 0.85 # B1 修订——target 域下限(P1 绝对锚)
const TARGET_ABS_FLOOR := 0.85 # 战斗结算 修订——target 域下限(P1 绝对锚)
func validate() -> Array:
@@ -53,7 +53,7 @@ func validate() -> Array:
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 规范合取形 + 雾占比绝对上限(B1 修订)
# 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]})
@@ -69,7 +69,7 @@ func validate() -> Array:
errs.append({"category": "cross_field", "message": "(%d,%d) cost_feasible 违例 attempts×W×H ≤ %d" % [L, D, COST_BUDGET]})
var biomes: Array = row.get("board_biomes", [])
if biomes.is_empty():
errs.append({"category": "cross_field", "message": "(%d,%d) board_biomes 非空域校验违例(A4⑮)" % [L, D]})
errs.append({"category": "cross_field", "message": "(%d,%d) board_biomes 非空域校验违例(地形变异⑮)" % [L, D]})
anchor_ok[L] = anchor_ok.get(L, false) or D == 0
for L in anchor_ok.keys():
if not anchor_ok[L]:
@@ -1,8 +1,8 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Date: 2026-09-12
# D2 DifficultyTable schema(C3 GDD Core Rule 1 兑现——B2 Rule 10 "接口未定 provisional" 的建表兑现,走 D2 接口漂移规则)。
# 全局修饰结构位:按 (L,D) 精确匹配施加乘数于 EnemyTable 行值(B2.spawn 实例化时、行值快照前)。
# MVP 空表(零行为——AC-10);首个使用场景 = 精英节点(C1 定稿联动,C3 OQ5);Boss 走 Dmax 直填行不走本表。
# 数据配置 DifficultyTable schema(难度曲线与Boss GDD Core Rule 1 兑现——敌人系统 Rule 10 "接口未定 provisional" 的建表兑现,走 数据配置 接口漂移规则)。
# 全局修饰结构位:按 (L,D) 精确匹配施加乘数于 EnemyTable 行值(敌人系统.spawn 实例化时、行值快照前)。
# MVP 空表(零行为——AC-10);首个使用场景 = 精英节点(关卡节点地图 定稿联动,难度曲线与Boss OQ5);Boss 走 Dmax 直填行不走本表。
class_name DifficultyTable
extends Resource
@@ -24,7 +24,7 @@ func validate() -> Array:
seen[key] = true
if L < 1 or D < 0:
errs.append({"category": "domain", "message": "难度键越域 (%d,%d)" % [L, D]})
# 乘数越域 = warning 级(使用时 clamp——组合事故非配置事故,C3 GDD Edge Case 预登记)
# 乘数越域 = warning 级(使用时 clamp——组合事故非配置事故,难度曲线与Boss GDD Edge Case 预登记)
for k in ["hp_mult", "atk_mult"]:
var v: float = float(row.get(k, 1.0))
if v < MULT_MIN or v > MULT_MAX:
@@ -1,7 +1,7 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
# Date: 2026-09-10
# D2 EnemyTable / HeroTable schema(B2 回填值)。EnemyTable 键 (L,D,enemy_id),每行集恰一敌种;
# 数据配置 EnemyTable / HeroTable schema(敌人系统 回填值)。EnemyTable 键 (L,D,enemy_id),每行集恰一敌种;
# HeroTable 单行全局(Rule 4 豁免)。依据 design/archive/gdd-2026-09-14/b2-enemy-system.md Schema 节。
class_name EnemyTable
extends Resource
@@ -39,7 +39,7 @@ func validate() -> Array:
var uc: int = int(row.get("unit_count", 0))
if uc < 1 or uc > 3:
errs.append({"category": "domain", "message": "%s unit_count 越域 [1,3]" % eid})
# unit_count_max(可省略——遭遇随机化批,C3 GDD Core Rule 8):区间 [unit_count, max],缺省 = 定值
# unit_count_max(可省略——遭遇随机化批,难度曲线与Boss GDD Core Rule 8):区间 [unit_count, max],缺省 = 定值
if row.has("unit_count_max"):
var ucm: int = int(row.get("unit_count_max", uc))
if ucm < uc or ucm > 3:
@@ -51,7 +51,7 @@ func validate() -> Array:
if atk < 0 or atk > 999:
errs.append({"category": "domain", "message": "%s atk 越域 [0,999]" % eid})
# atk_min/atk_max(可省略配对——2026-09-14 敌方攻击区间批):遭遇骰同流 spawn 期整战持久 roll;
# 中点锚 = A3 条款①同款期望当量纪律(调区间必同步调 atk)
# 中点锚 = 行动经济 条款①同款期望当量纪律(调区间必同步调 atk)
var has_amin: bool = row.has("atk_min")
var has_amax: bool = row.has("atk_max")
if has_amin != has_amax:
@@ -1,13 +1,13 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
# Date: 2026-09-10
# D2 HeroTable schema(B2 接口漂移新增;单行全局,Rule 4 豁免)。
# 数据配置 HeroTable schema(敌人系统 接口漂移新增;单行全局,Rule 4 豁免)。
# atk = 基础攻击恒存在(2026-09-11 用户裁决):域 [1,999]、0 = 加载拒绝——零基线死锁 schema 层杜绝。
class_name HeroTable
extends Resource
@export var max_hp: int = 50 # [1,999] knob(provisional,溯源 = 原型锚)
@export var atk: int = 6 # [1,999] knob——基础攻击(溯源 = 概念原型 HERO_BASE_ATK,provisional 归 C3);消费面 = B1 战斗段先手攻击
@export var atk: int = 6 # [1,999] knob——基础攻击(溯源 = 概念原型 HERO_BASE_ATK,provisional 归 难度曲线与Boss);消费面 = 战斗结算·战斗段先手攻击
func validate() -> Array:
@@ -1,8 +1,8 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 线性第一关(战×2→道具→战×2→恢复→Boss)能否承载"数值提升→通关期望改善"的元进度验证
# Date: 2026-09-11
# D2 LevelTable schema(provisional——C1 关卡节点地图未设计,本表为线性切片形态的结构位:
# nodes 数组即未来节点链的数据承载面;路径分支/奖励点分布的定稿权归 C1/C3)。
# 数据配置 LevelTable schema(provisional——关卡节点地图未设计,本表为线性切片形态的结构位:
# nodes 数组即未来节点链的数据承载面;路径分支/奖励点分布的定稿权归 关卡节点地图/难度曲线与Boss)。
# 节点类型 closed enum:battle(战斗,须带 node_depth)/ reward(随机主动道具点)/ rest(血量恢复点)/ boss(关底 Boss,须带 node_depth)。
class_name LevelTable
extends Resource
@@ -51,7 +51,7 @@ func validate() -> Array:
errs.append({"category": "structural", "message": "level %d Boss 节点数 = %d(须恰一)" % [L, boss_count]})
if battle_count < 2:
errs.append({"category": "structural", "message": "level %d 战斗节点(含 Boss)= %d(≥2 才构成关卡)" % [L, battle_count]})
# 线性切片纪律:末节点必须是 Boss(关底收尾——C2 关卡定义)
# 线性切片纪律:末节点必须是 Boss(关底收尾——冒险结构 关卡定义)
if String(nodes[nodes.size() - 1].get("type", "")) != "boss":
errs.append({"category": "structural", "message": "level %d 末节点非 Boss(线性关卡以 Boss 收尾)" % L})
return errs
@@ -1,7 +1,7 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
# Date: 2026-09-10
# D2 RewardConfigTable schema(B4 full 评审接口漂移新增;单行全局——B4 标量 knob 的 Rule 1 载体)。
# 数据配置 RewardConfigTable schema(奖励道具经济 full 评审接口漂移新增;单行全局——奖励道具经济 标量 knob 的 Rule 1 载体)。
class_name RewardConfigTable
extends Resource
@@ -1,7 +1,7 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
# Date: 2026-09-10
# D2 RewardTable / RewardConfigTable schema(B4 回填值)。
# 数据配置 RewardTable / RewardConfigTable schema(奖励道具经济 回填值)。
# RewardTable 行 = Dictionary,键 reward_id(Rule 4 豁免:定义表);跨字段校验族 + 池护栏。
class_name RewardTable
extends Resource
@@ -68,6 +68,6 @@ func validate() -> Array:
if cat == "buff":
errs.append({"category": "cross_field", "message": "%s buff 行禁区间字段(power 恒 0)" % rid})
if power != int((pmin + pmax) / 2.0):
# 中点锚:power = 区间中点(A3 条款①期望当量锚——调区间必同步调 power)
# 中点锚:power = 区间中点(行动经济 条款①期望当量锚——调区间必同步调 power)
errs.append({"category": "cross_field", "message": "%s power(%d) ≠ 区间中点(%d)——中点恒等期望当量锚" % [rid, power, (pmin + pmax) / 2]})
return errs
@@ -1,7 +1,7 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
# Date: 2026-09-10
# D2 SettlementTable schema(B1 回填值;单行全局,Rule 4 豁免)——八字段口径。
# 数据配置 SettlementTable schema(战斗结算 回填值;单行全局,Rule 4 豁免)——八字段口径。
# 三行致命 manifest:segorder_violation / poolmap_dangling / pperfect_oob。
class_name SettlementTable
extends Resource
@@ -1,7 +1,7 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
# Date: 2026-09-10
# D2 StatusEffectTable schema(B3 回填值;行 = Dictionary,键 effect_id;Rule 4 豁免)。
# 数据配置 StatusEffectTable schema(增益状态 回填值;行 = Dictionary,键 effect_id;Rule 4 豁免)。
# 依据 design/archive/gdd-2026-09-14/b3-status-effect.md "StatusEffectTable Schema 定稿"。
class_name StatusEffectTable
extends Resource
@@ -1,7 +1,7 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
# Date: 2026-09-10
# D2 VariationTable schema(A4 回填值;行 = Dictionary,键 variant_id)。
# 数据配置 VariationTable schema(地形变异 回填值;行 = Dictionary,键 variant_id)。
# 依据 design/archive/gdd-2026-09-14/a4-terrain-variation.md "VariationTable Schema 定稿"。
class_name VariationTable
extends Resource
@@ -38,7 +38,7 @@ func validate() -> Array:
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")]})
# 跨字段(A4 re-review 增补,加载期致命):hide_numbers ⇒ safe_nonzero
# 跨字段(地形变异 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})
return errs
@@ -1,7 +1,7 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Date: 2026-09-13(封装审查 O1/O2 修复批——自 main.gd 拆出,规范 1.1 审视线 / 单一实现纪律)
# 调试模式编排控制器:单场遭遇(第 1/2 层)与连续两层(库存与 HP 关内延续)的模式状态机与战斗生命周期。
# 职责边界:层序/携带 HP/共享 B4 状态 + B1 生命周期编排(创建 / begin_battle / 关卡末 level_clear)。
# 职责边界:层序/携带 HP/共享 奖励道具经济 状态 + 战斗结算 生命周期编排(创建 / begin_battle / 关卡末 level_clear)。
# 授权先例:与 tune_sim_controller 同款(code-review 2026-09-12 批)——控制器承接模式编排,
# 组合根仍持有全部信号接线(battle_created 先于 begin_battle 发出,ADR-0001 接线唯一归组合根)
# 与终局 UI 决策;玩家意图写调用(settle_batch / 窗口提交 / 刷新)不经本类。
@@ -10,8 +10,8 @@
# O2 收口:调试两层的"库存与 HP 延续"语义唯一实现点(此前与 main.gd 调试分支双实现漂移)。
extends RefCounted
signal battle_created(b1, level: int, carry_hp: int) # begin_battle 前——组合根完成信号接线(ADR-0001)
signal battle_ready(b1, level: int, carry_hp: int) # begin_battle 完成后——组合根 rebuild 棋盘 + 刷新呈现
signal battle_created(settlement, level: int, carry_hp: int) # begin_battle 前——组合根完成信号接线(ADR-0001)
signal battle_ready(settlement, level: int, carry_hp: int) # begin_battle 完成后——组合根 rebuild 棋盘 + 刷新呈现
var db
var rng # /root/RngService(组合根注入)
@@ -22,8 +22,8 @@ var _level_mode := false
var _level_battles: Array = []
var _level_idx := 0
var _carry_hp := 0
var _shared_b4 = null
var b1 = null # 当前战斗编排器(组合根在 battle_created 接线、battle_ready 接手)
var _shared_reward_economy = null
var settlement = null # 当前战斗编排器(组合根在 battle_created 接线、battle_ready 接手)
func _init(db_ref, rng_ref, hero_override: Callable, seed_value: Callable) -> void:
@@ -41,18 +41,18 @@ func is_level_mode() -> bool:
# 无库存延续、standalone 终局语义保留供探针战斗收尾)
func start_single(level: int) -> bool:
_level_mode = false
_shared_b4 = null
_shared_reward_economy = null
return _start_battle(level, -1)
# 连续两层(调试入口——共享 B4 + HP 携带;首层满血开局)
# 连续两层(调试入口——共享 奖励道具经济 + HP 携带;首层满血开局)
func start_debug_levels() -> bool:
_level_mode = true
_level_battles = [1, 2]
_level_idx = 0
_carry_hp = 0
rng.new_adventure(_seed_value.call())
_shared_b4 = RewardEconomy.new(db, rng)
_shared_reward_economy = RewardEconomy.new(db, rng)
return _start_battle(_level_battles[0], -1)
@@ -69,13 +69,13 @@ func on_battle_finished(payload: Dictionary) -> Dictionary:
_carry_hp = maxi(1, int(payload.get("hero_hp", 0))) # despawn 前捕获值
_level_idx += 1
return {"outcome": "next_layer", "layer": _level_idx, "carry_hp": _carry_hp,
"items_left": b1.stock_count()}
"items_left": settlement.stock_count()}
var remaining := 0
var is_level_end := result == "victory" and _level_mode
if is_level_end:
remaining = int(b1.level_clear()["remaining"]) # 库存随关卡结束清空(关内延续裁决③)
remaining = int(settlement.level_clear()["remaining"]) # 库存随关卡结束清空(关内延续裁决③)
elif _level_mode:
b1.level_clear() # 失败同样清库(关卡域生命周期闭合——调试模式与 runner 模式同规)
settlement.level_clear() # 失败同样清库(关卡域生命周期闭合——调试模式与 runner 模式同规)
return {"outcome": "level_end" if is_level_end else "standalone_end",
"layer": _level_idx, "carry_hp": _carry_hp, "items_left": 0, "remaining": remaining}
@@ -83,20 +83,20 @@ func on_battle_finished(payload: Dictionary) -> Dictionary:
# 会话复位(组合根终局出口"返回开始界面"调用)
func reset() -> void:
_level_mode = false
_shared_b4 = null
b1 = null
_shared_reward_economy = null
settlement = null
func _start_battle(level: int, carry_hp: int) -> bool:
if not _level_mode or _level_idx == 0:
rng.new_adventure(_seed_value.call())
var ov: Dictionary = _hero_override.call() # E4:生效值经 encounter 传入——零表写
b1 = BattleOrchestrator.new(db, rng, _shared_b4 if _level_mode else null)
battle_created.emit(b1, level, carry_hp)
settlement = BattleOrchestrator.new(db, rng, _shared_reward_economy if _level_mode else null)
battle_created.emit(settlement, level, carry_hp)
var hp: int = carry_hp if carry_hp > 0 else int(ov.max_hp)
if not b1.begin_battle({"level_index": level, "node_depth": 0, "hero_hp": hp,
if not settlement.begin_battle({"level_index": level, "node_depth": 0, "hero_hp": hp,
"hero_atk": int(ov.atk), "hero_max_hp": int(ov.max_hp)}):
push_error("[DebugSession] 战斗启动失败(level %d)" % level)
return false
battle_ready.emit(b1, level, carry_hp)
battle_ready.emit(settlement, level, carry_hp)
return true
@@ -1,14 +1,14 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 实现 GDD:design/ux/battle.md(五区呈现装配器)——读核心只读面(B1 呈现快照 / runner)→
# 实现 GDD:design/ux/battle.md(五区呈现装配器)——读核心只读面(战斗结算 呈现快照 / runner)→
# 组装文案/投影 → 驱动各视图 setter。
# 高内聚边界:"核心状态 → 呈现文案"的全部翻译集中于此;视图保持零核心依赖;
# 静态无状态——呈现不持久化任何核心句柄。
# E1 封装收口(2026-09-13):核心状态读取统一经 B1.presentation_snapshot() 成文快照面——
# 禁再链式穿透 b1.b2.hero / b1.b2.enemies / b1.b3.query_active 一类路径(B1 协作对象已私有化,
# E1 封装收口(2026-09-13):核心状态读取统一经 战斗结算.presentation_snapshot() 成文快照面——
# 禁再链式穿透 settlement.enemy_system.hero / settlement.enemy_system.enemies / settlement.status_effects.query_active 一类路径(战斗结算 协作对象已私有化,
# 敌人含死亡残位的投影由快照 enemies[] 承载,含 alive/beat/ring 字段)。
# 更新触发注记(battle.md Data Requirements):旗/拔/移动成功反馈零事件外发(A2 发射面封闭)——
# 更新触发注记(battle.md Data Requirements):旗/拔/移动成功反馈零事件外发(排雷玩法 发射面封闭)——
# 经 ActionOutcome 返回值命令式驱动;AP/RAP 无推送信号——组合根每次调用返回后重查;
# B3 纯被动容器零信号——状态栏刷新绑 B1 结算编排完成时点;库存 B4 查询面——写路径回执后重查。
# 增益状态 纯被动容器零信号——状态栏刷新绑 战斗结算 结算编排完成时点;库存 奖励道具经济 查询面——写路径回执后重查。
# Date: 2026-09-12(五区改造批;基础 = 2026-09-11 HUD 装配器;2026-09-13 快照面改造)
class_name HudPresenter
extends RefCounted
@@ -16,11 +16,11 @@ extends RefCounted
# 每次结算行/轮次/节点变化/动作返回后调用(组合根全量重查装配)
static func refresh(stage: BattleStageView, info: HeroInfoPanelView, ap_bar: ApBarView,
inv: InventoryPanelView, b1, runner, level_active: bool, db, refresh_remaining: int) -> void:
if b1 == null:
inv: InventoryPanelView, settlement, runner, level_active: bool, db, refresh_remaining: int) -> void:
if settlement == null:
_refresh_out_of_battle(stage, info, inv, runner, level_active, db)
return
var s: Dictionary = b1.presentation_snapshot()
var s: Dictionary = settlement.presentation_snapshot()
var hero: Dictionary = s.hero
if hero.is_empty() or not bool(hero.get("alive", false)):
_refresh_out_of_battle(stage, info, inv, runner, level_active, db)
@@ -65,7 +65,7 @@ static func _hero_status_summary(effects: Array) -> String:
static func _refresh_economy(info: HeroInfoPanelView, ap_bar: ApBarView, s: Dictionary, db, refresh_remaining: int) -> void:
ap_bar.set_ap(int(s.ap), int(db.action_economy.ap_per_round))
# RAP 活值投影(C 区点阵)——清零反馈分支(A3 D1 义务)依赖本处持续投喂才会可达
# RAP 活值投影(C 区点阵)——清零反馈分支(行动经济 D1 义务)依赖本处持续投喂才会可达
info.set_rap(int(s.rap), int(db.action_economy.rap_refresh_cost))
var digging := String(s.phase) == ActionEconomy.PHASE_ACTIVE
ap_bar.set_refresh_state(refresh_remaining,
@@ -110,7 +110,7 @@ static func _inventory_row(name_: String, category: String, power: int, status:
return {"display_name": name_, "category": category, "desc": desc, "tooltip": tooltip, "status": status}
# 窗口条目投影(B1 window_opened 载荷 → E 区窗口态行;pos = 窗口条目位置——缺陷④契约)
# 窗口条目投影(战斗结算 window_opened 载荷 → E 区窗口态行;pos = 窗口条目位置——缺陷④契约)
static func window_rows(items: Array, hero_hp: int, hero_max_hp: int) -> Array:
var rows: Array = []
for pos in items.size():
@@ -133,7 +133,7 @@ static func _refresh_out_of_battle(stage: BattleStageView, info: HeroInfoPanelVi
info.set_attack(runner.effective_atk(), "(关卡推进中)")
stage.hero_card.set_data(runner.hero_hp, runner.effective_max_hp(), runner.effective_atk(), "(关卡推进中)", "")
stage.clear_enemies() # 节点间隙清残位敌卡(死亡瞬间的灰化呈现已在结算行驱动完成)
info.set_rap(0, int(db.action_economy.rap_refresh_cost)) # battle_over RAP 清零反馈(A3 义务)
info.set_rap(0, int(db.action_economy.rap_refresh_cost)) # battle_over RAP 清零反馈(行动经济 义务)
var rows: Array = []
for inst in runner.inventory():
if inst.status == "InStock":
@@ -144,7 +144,7 @@ static func _refresh_out_of_battle(stage: BattleStageView, info: HeroInfoPanelVi
inv.show_inventory(rows)
# 三源拒绝可感知(A2 reject_reason/sub_reason → 玩家文案;pattern 1 双源拒绝反馈)
# 三源拒绝可感知(排雷玩法 reject_reason/sub_reason → 玩家文案;pattern 1 双源拒绝反馈)
static func outcome_text(outcome: Dictionary) -> String:
if outcome.accepted:
return "执行完成。"
@@ -1,21 +1,21 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 线性第一关(战×2→道具→战×2→恢复→Boss)能否承载"数值提升→通关期望改善"的元进度验证
# 实现 GDD:design/ux/battle.md(战斗屏五区 A/B/C/D/E + 全屏 States + 遥测)。
# Date: 2026-09-12(battle.md 五区改造批;基础 = 2026-09-11 D1-lite 组合根;code-review 修复批重构)
# 2026-09-13(封装审查修复批):核心状态读取收口至 B1/runner 成文只读面(presentation_snapshot /
# current_board / current_battle 等——禁链式穿透 b1.b2.hero 一类路径);调试模式编排抽离
# Date: 2026-09-12(battle.md 五区改造批;基础 = 2026-09-11 HUD框架-lite 组合根;code-review 修复批重构)
# 2026-09-13(封装审查修复批):核心状态读取收口至 战斗结算/runner 成文只读面(presentation_snapshot /
# current_board / current_battle 等——禁链式穿透 settlement.enemy_system.hero 一类路径);调试模式编排抽离
# debug_session.gd(规范 1.1 审视线 + 调试语义单一实现)。
# 组合根(composition root):唯一的模式流转与 core↔view 接线处。
# 职责边界:① 构建五区视图并接线信号(节点构建归 ui_assembler.gd——规范 1.1,本类只接线)
# ② 模式管理(完整关卡 C1 runner / 调试两层——后者编排归 debug_session.gd;单场原语仅探针使用)
# ③ 核心写调用只在此处发生(a2.settle_batch / b1.submit_window_selection / 刷新编排入口 / runner 推进;
# ② 模式管理(完整关卡 关卡节点地图 runner / 调试两层——后者编排归 debug_session.gd;单场原语仅探针使用)
# ③ 核心写调用只在此处发生(minesweeper.settle_batch / settlement.submit_window_selection / 刷新编排入口 / runner 推进;
# E4 收口:加成值零表写——面板百分比经 runner.set_hero_override / debug_session 生效值 Callable
# 以 encounter 参数入战斗,db.hero 运行期只读)
# ④ 全屏 States 状态机(挖掘/结算/道具窗口/终局)与一次性提示去重。
# 渲染与文案归 views/,核心状态→文案装配归 hud_presenter.gd 与 battle_narrator.gd,节点构建归
# ui_assembler.gd,模态层级归 modal_stack.gd,批量模拟编排归 tune_sim_controller.gd,调试模式编排归
# debug_session.gd——本类不拼呈现字符串。
# ADR-0001 纪律:呈现层永不直接订阅 A2/A3——只订 B1/runner 转发信号;相位读取仅用于驱动视图状态。
# ADR-0001 纪律:呈现层永不直接订阅 排雷玩法/行动经济——只订 战斗结算/runner 转发信号;相位读取仅用于驱动视图状态。
extends Control
const UiAssembler := preload("res://presentation/ui_assembler.gd")
@@ -25,8 +25,8 @@ const TuneSimController := preload("res://presentation/tune_sim_controller.gd")
const DebugSessionScript := preload("res://presentation/debug_session.gd")
var db
var b1 = null # 当前战斗编排器(关卡与调试模式统一读它)
var runner = null # C1 线性关卡推进器(完整关卡模式)
var settlement = null # 当前战斗编排器(关卡与调试模式统一读它)
var runner = null # 关卡节点地图 线性关卡推进器(完整关卡模式)
var logger = null # 试玩日志器(JSONL 持久化 + 卡死诊断转储——呈现层组件)
var _level_active := false
var _seed_edit: LineEdit
@@ -102,7 +102,7 @@ func _build_ui() -> void:
inventory_panel.skipped.connect(_on_window_skipped)
inventory_panel.reject_reason.connect(_on_inventory_reject)
# 奖励徽章数据源:B4 身份解析只读回调(已开格身份 icon——挖掘期 Active 相位内有效)
# 奖励徽章数据源:奖励道具经济 身份解析只读回调(已开格身份 icon——挖掘期 Active 相位内有效)
board_view.set_identity_resolver(_identity_lookup)
logger = PlaytestLogger.new()
@@ -174,10 +174,10 @@ func _input_locked() -> bool:
# 模态/非战斗期棋盘输入锁(各 overlay 自身可见即锁定)
return legend_overlay.is_open() or review_overlay.is_open() or _end_overlay.is_open() \
or node_overlay.visible or tune_panel.visible or _start_overlay.visible \
or _inter_overlay.visible or b1 == null
or _inter_overlay.visible or settlement == null
# ==================== 完整关卡模式(C1 线性推进器)====================
# ==================== 完整关卡模式(关卡节点地图 线性推进器)====================
func _start_full_level() -> void:
_start_overlay.visible = false
@@ -194,7 +194,7 @@ func _start_full_level() -> void:
_reset_run_scope()
log_view.clear()
var seed_val := _seed_value()
logger.start_session(seed_val) # 各战斗的 attach 在 _on_runner_node 内(b1 逐战创建)
logger.start_session(seed_val) # 各战斗的 attach 在 _on_runner_node 内(settlement 逐战创建)
if not runner.start_level(1, seed_val):
_fatal_screen("关卡启动失败——见输出日志")
return
@@ -214,8 +214,8 @@ func _reset_run_scope() -> void:
func _on_runner_node(node: Dictionary, info: Dictionary) -> void:
var t := String(node.get("type", ""))
if t == "battle" or t == "boss":
b1 = runner.current_battle() # 呈现层统一读 b1(与调试模式共用接线)
logger.attach_battle(b1)
settlement = runner.current_battle() # 呈现层统一读 settlement(与调试模式共用接线)
logger.attach_battle(settlement)
_boss_active = t == "boss"
_show_board_fx_for_node(t)
log_view.append_bb(Narrator.node_banner(runner, node, t))
@@ -237,12 +237,12 @@ func _on_runner_node(node: Dictionary, info: Dictionary) -> void:
func _on_level_finished(payload: Dictionary) -> void:
_level_active = false
var result: String = String(payload.get("result", ""))
b1 = null
settlement = null
_sync_settlement_visuals() # → 全关
info_panel.set_status_cleared_feedback() # 状态槽全清可见反馈(B3×4④)
info_panel.set_status_cleared_feedback() # 状态槽全清可见反馈(增益状态×4④)
stage.gray_all_enemies() # 死亡兜底:卡体灰化残位(禁瞬消语义的战斗收尾面)
if result == "victory":
# 关卡结算:库存清空可见反馈(B4 D1×⑤)+ 胜利终局 overlay(金蜡封落于棋盘中央)
# 关卡结算:库存清空可见反馈(奖励道具经济 D1×⑤)+ 胜利终局 overlay(金蜡封落于棋盘中央)
var remaining: int = int(payload.get("remaining_items", 0))
inventory_panel.play_cleared_feedback(remaining)
_end_overlay.open_victory(Narrator.level_end_text("victory", payload, runner.effective_max_hp()),
@@ -265,7 +265,7 @@ func _on_battle_end_continue() -> void:
if runner != null:
runner.restore_hero_base()
runner = null
b1 = null
settlement = null
tune_panel.reset_sliders()
strip_view.refresh(null)
_start_overlay.visible = true
@@ -289,16 +289,16 @@ func _start_debug_levels() -> void:
_fatal_screen("战斗启动失败——见输出日志")
func _on_debug_battle_created(b1_ref, _level: int, _carry: int) -> void:
# begin_battle 前接手 + 接线(ADR-0001:signal.connect 唯一归组合根)。b1 必须先于 begin_battle
func _on_debug_battle_created(settlement_ref, _level: int, _carry: int) -> void:
# begin_battle 前接手 + 接线(ADR-0001:signal.connect 唯一归组合根)。settlement 必须先于 begin_battle
# 就位——begin 期间发出的 board_changed 要用新板重建棋盘并触发雾首遇提示(缺陷①同款时序义务)
b1 = b1_ref
_wire_battle(b1_ref)
settlement = settlement_ref
_wire_battle(settlement_ref)
func _on_debug_battle_ready(b1_ref, _level: int, _carry: int) -> void:
b1 = b1_ref
board_view.rebuild(b1.a2.current_board(), b1.a2)
func _on_debug_battle_ready(settlement_ref, _level: int, _carry: int) -> void:
settlement = settlement_ref
board_view.rebuild(settlement.minesweeper.current_board(), settlement.minesweeper)
_check_board_hints()
_refresh_presentations()
@@ -320,11 +320,11 @@ func _scaled(base: int, pct: int) -> int:
return maxi(1, int(round(float(base) * (100.0 + float(pct)) / 100.0)))
# 奖励徽章身份解析(组合根守卫:仅挖掘期 Active 相位内查询——B4 Ready 域外零读、零诊断噪音)
# 奖励徽章身份解析(组合根守卫:仅挖掘期 Active 相位内查询——奖励道具经济 Ready 域外零读、零诊断噪音)
func _identity_lookup(c: Vector2i):
if b1 == null or b1.a2.current_board() == null or b1.a3.phase != b1.a3.PHASE_ACTIVE:
if settlement == null or settlement.minesweeper.current_board() == null or settlement.action_economy.phase != settlement.action_economy.PHASE_ACTIVE:
return null
return b1.query_reward_identity(c)
return settlement.query_reward_identity(c)
func _show_board_fx_for_node(node_type: String = "") -> void:
@@ -334,12 +334,12 @@ func _show_board_fx_for_node(node_type: String = "") -> void:
func _on_battle_finished(payload: Dictionary) -> void:
# 仅调试模式接线(完整关卡的结束归 C1 runner._on_level_finished)
# 仅调试模式接线(完整关卡的结束归 关卡节点地图 runner._on_level_finished)
var result: String = String(payload.get("result", ""))
_sync_settlement_visuals() # → 全关(battle_over 反馈面)
stage.gray_all_enemies() # 死亡兜底:卡体灰化残位(禁瞬消语义的战斗收尾面)
info_panel.set_rap(0, int(db.action_economy.rap_refresh_cost)) # RAP 清零滚动 + 微文案
info_panel.set_status_cleared_feedback() # 状态槽全清可见反馈(B3×4④)
info_panel.set_status_cleared_feedback() # 状态槽全清可见反馈(增益状态×4④)
var decision: Dictionary = _debug.on_battle_finished(payload)
match String(decision.outcome):
"next_layer":
@@ -365,18 +365,18 @@ func _on_battle_finished(payload: Dictionary) -> void:
# ==================== core↔view 接线(战斗信号统一走这里)====================
func _wire_battle(b1_ref) -> void:
b1_ref.step_row.connect(_on_step_row)
b1_ref.window_opened.connect(_on_window_opened)
b1_ref.battle_finished.connect(_on_battle_finished)
b1_ref.round_started.connect(_on_round_started)
b1_ref.board_changed.connect(_on_board_changed)
func _wire_battle(settlement_ref) -> void:
settlement_ref.step_row.connect(_on_step_row)
settlement_ref.window_opened.connect(_on_window_opened)
settlement_ref.battle_finished.connect(_on_battle_finished)
settlement_ref.round_started.connect(_on_round_started)
settlement_ref.board_changed.connect(_on_board_changed)
func _on_step_row(row: Dictionary) -> void:
log_view.append_row(row)
stage.on_step_row_fx(row)
# 高光演出(B1×3①② 占位):完美排雷横幅 / 标中旗加重弹道
# 高光演出(战斗结算×3①② 占位):完美排雷横幅 / 标中旗加重弹道
if String(row.get("type", "")) == "highlight":
board_zone.fx.play_perfect_sweep()
elif String(row.get("highlight", "")) == "flag_hit":
@@ -392,12 +392,12 @@ func _on_round_started(ri: int) -> void:
func _on_board_changed() -> void:
# 关卡模式下 board_changed 可能先于主场景接手 b1 到达(begin_battle 期间发)——先从 runner 同步
if b1 == null and _level_active and runner != null:
b1 = runner.current_battle()
if b1 == null:
# 关卡模式下 board_changed 可能先于主场景接手 settlement 到达(begin_battle 期间发)——先从 runner 同步
if settlement == null and _level_active and runner != null:
settlement = runner.current_battle()
if settlement == null:
return
board_view.rebuild(b1.a2.current_board(), b1.a2)
board_view.rebuild(settlement.minesweeper.current_board(), settlement.minesweeper)
_check_board_hints()
_refresh_presentations()
@@ -405,13 +405,16 @@ func _on_board_changed() -> void:
# ---- 一次性非模态提示(每 run 一次——pattern 10)与诚实提示 ----
func _check_board_hints() -> void:
var board = b1.a2.current_board()
var board = settlement.minesweeper.current_board()
if board == null:
return
# 零合法板(A1 EC5 降级)诚实提示——非静默(toast,非模态)
# 零合法板(棋盘生成 EC5 降级)诚实提示——非静默(toast,非模态)
if board.illegal_product:
board_zone.toast.show_toast("本板为零合法板降级产物——出图器已尽力,数据面诚实告知", "warn")
# 雾首遇(每 run 一次;必含三要点:无雷盲区不赌命 / 雾格不传播连锁 / 雾内插旗纯损——A4 D1×②)
# 接受最优降级(有合法板但无一达标)——降级标记独立于非法产物标记,同走诚实告知
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")
@@ -442,7 +445,7 @@ func _show_hint(text: String, hint_id: String) -> void:
func _on_window_opened(items: Array) -> void:
_window_payload = items
var hero: Dictionary = b1.presentation_snapshot().hero # E1 收口:经呈现快照面读 HP
var hero: Dictionary = settlement.presentation_snapshot().hero # E1 收口:经呈现快照面读 HP
var hp: int = int(hero.get("hp", 0)) if not hero.is_empty() else 0
var max_hp: int = int(hero.get("max_hp", 0)) if not hero.is_empty() else 1
inventory_panel.open_window(HudPresenter.window_rows(items, hp, max_hp))
@@ -456,16 +459,16 @@ func _on_window_item_selected(pos: int) -> void:
logger.log_ux("ux_window_choice", {"selected_item_id": String(it.get("item_id", "")),
"usable_now": true, "skip": false})
inventory_panel.close_window()
if b1 != null:
b1.submit_window_selection([pos])
if settlement != null:
settlement.submit_window_selection([pos])
_refresh_presentations()
func _on_window_skipped() -> void:
logger.log_ux("ux_window_choice", {"selected_item_id": "", "usable_now": false, "skip": true})
inventory_panel.close_window()
if b1 != null:
b1.submit_window_selection([])
if settlement != null:
settlement.submit_window_selection([])
_refresh_presentations()
@@ -485,13 +488,13 @@ func _on_board_action(cell: Vector2i, kind: String) -> void:
# 玩家动作 → 核心写调用的唯一通道(模态/窗口期/无战斗拒绝在此收口)
if _input_locked() or inventory_panel.is_window_open():
return
var outcomes: Array = b1.a2.settle_batch([{"action": kind, "cell": cell}])
var outcomes: Array = settlement.minesweeper.settle_batch([{"action": kind, "cell": cell}])
var accepted: bool = not outcomes.is_empty() and bool(outcomes[0].accepted)
logger.log_action(kind, cell, accepted)
_handle_outcome_feedback(outcomes[0] if not outcomes.is_empty() else {})
# settle_batch 内部同步走完 结算→战斗结束→关卡推进(事件同步发射)——关卡终局时 b1 已被
# runner 置空,清图演出判定须先判 b1(基线既有重入隐患,2026-09-13 加固)
if accepted and kind == "dig" and b1 != null and b1.a2.current_board() != null and b1.a2.clear_map_cleared():
# settle_batch 内部同步走完 结算→战斗结束→关卡推进(事件同步发射)——关卡终局时 settlement 已被
# runner 置空,清图演出判定须先判 settlement(基线既有重入隐患,2026-09-13 加固)
if accepted and kind == "dig" and settlement != null and settlement.minesweeper.current_board() != null and settlement.minesweeper.clear_map_cleared():
board_zone.fx.play_clear_burst()
board_view.refresh()
_refresh_presentations()
@@ -500,8 +503,8 @@ func _on_board_action(cell: Vector2i, kind: String) -> void:
func _on_stop_pressed() -> void:
if _input_locked() or inventory_panel.is_window_open():
return
# stop 经批次提交(A2 D1×6①——不直调 A3)
var outcomes: Array = b1.a2.settle_batch([{"action": "stop"}])
# stop 经批次提交(排雷玩法 D1×6①——不直调 行动经济)
var outcomes: Array = settlement.minesweeper.settle_batch([{"action": "stop"}])
logger.log_action("stop", Vector2i(-1, -1), not outcomes.is_empty() and bool(outcomes[0].accepted))
if not outcomes.is_empty() and String(outcomes[0].get("outcome", "")) == "voided":
# voided ≠ 拒绝——独立文案(pattern 1:防静默改判读感欺骗)
@@ -518,16 +521,16 @@ func _on_refresh_confirmed() -> void:
var ok: bool = false
if _level_active and runner != null:
ok = runner.player_request_refresh()
elif b1 != null:
ok = b1.player_request_refresh()
elif settlement != null:
ok = settlement.player_request_refresh()
logger.log_action("refresh", Vector2i(-1, -1), ok)
if ok:
_refresh_used_round += 1
logger.log_ux("ux_refresh_attempt", {"result": "accepted", "round_refresh_index": _refresh_used_round})
board_zone.toast.show_toast("RAP−8 · 旧板旗作废——新图已就位,AP 保留继续挖", "gold")
else:
# 资源面/上限拒绝(EC13 合法拒绝可见;A3 无载荷——客户端按上下文推断)
var rap: int = int(b1.a3.rap) if b1 != null else 0
# 资源面/上限拒绝(EC13 合法拒绝可见;行动经济 无载荷——客户端按上下文推断)
var rap: int = int(settlement.action_economy.rap) if settlement != null else 0
var cost: int = int(db.action_economy.rap_refresh_cost)
var result := "rejected_rap" if rap < cost else "rejected_cap"
logger.log_ux("ux_refresh_attempt", {"result": result, "round_refresh_index": _refresh_used_round + 1})
@@ -556,7 +559,7 @@ func _handle_outcome_feedback(outcome: Dictionary) -> void:
func _refresh_presentations() -> void:
# 每个核心信号/写调用返回后的全量重查装配(更新触发注记——无推送信号,命令式重查)
var refresh_remaining: int = maxi(0, int(db.action_economy.refresh_max_per_round) - _refresh_used_round)
HudPresenter.refresh(stage, info_panel, ap_bar, inventory_panel, b1, runner, _level_active, db, refresh_remaining)
HudPresenter.refresh(stage, info_panel, ap_bar, inventory_panel, settlement, runner, _level_active, db, refresh_remaining)
_sync_settlement_visuals()
@@ -571,17 +574,17 @@ func _sync_settlement_visuals() -> void:
func _is_settling() -> bool:
if b1 == null or not b1.is_battle_live():
if settlement == null or not settlement.is_battle_live():
return false
return b1.a3.phase == b1.a3.PHASE_WAITING
return settlement.action_economy.phase == settlement.action_economy.PHASE_WAITING
func _phase_text() -> String:
# 文案装配归 narrator(b1 缺位 = 非战斗态——组合根模式状态守卫)
if b1 == null:
# 文案装配归 narrator(settlement 缺位 = 非战斗态——组合根模式状态守卫)
if settlement == null:
return "第 — 轮 · —"
return Narrator.phase_text(int(b1.round_index), String(b1.battle_result),
inventory_panel.is_window_open(), b1.a3.phase == b1.a3.PHASE_WAITING,
return Narrator.phase_text(int(settlement.round_index), String(settlement.battle_result),
inventory_panel.is_window_open(), settlement.action_economy.phase == settlement.action_economy.PHASE_WAITING,
_boss_active, Narrator.boss_name(runner, db))
@@ -604,9 +607,9 @@ func _on_diagnose_pressed() -> void:
var ctx := {"mode": "level" if _level_active else "debug", "seed": _seed_value()}
if _level_active and runner != null:
ctx["runner"] = {"node_index": runner.node_index, "level_result": String(runner.level_result),
"hero_hp": runner.hero_hp, "b1_is_null": runner.current_battle() == null}
if b1 != null:
ctx["b1"] = b1.diagnostic_snapshot() # 诊断观测面(私有字段零直读——code-review 2026-09-12)
"hero_hp": runner.hero_hp, "settlement_is_null": runner.current_battle() == null}
if settlement != null:
ctx["settlement"] = settlement.diagnostic_snapshot() # 诊断观测面(私有字段零直读——code-review 2026-09-12)
var path: String = logger.dump_diagnosis(ctx)
board_zone.toast.show_toast("已转储诊断: " + path, "info")
@@ -2,11 +2,11 @@
# 实现 GDD:design/ux/battle.md(§Events Fired 遥测义务——ux 行族)。
# Date: 2026-09-12(五区改造批增 ux 遥测;基础 = 2026-09-12 日志器首版)
# 试玩日志器(呈现层组件——订阅核心信号持久化 JSONL,供数值分析;卡死诊断转储)。
# 职责边界:核心层只发 settlement_snapshot 信号(B1)/既有信号(battle_finished/node_entered/
# 职责边界:核心层只发 settlement_snapshot 信号(战斗结算)/既有信号(battle_finished/node_entered/
# level_finished/step_row);本组件负责订阅、缓冲与写文件——核心零日志感知(零 UI 依赖不变)。
# 文件 = user://logs/playtest_<seed>_<时间戳>.jsonl(每行一次结算/终局/节点的全量状态 JSON)。
# ux 行族(呈现层观测,不改 core):ux_round_input_ratio(原始输入动作数∶AP 消耗)、
# ux_round_exit{path_tag}(快照 round_facts.path——呈现层只订 B1 的 ADR 纪律下唯一合法来源)、
# ux_round_exit{path_tag}(快照 round_facts.path——呈现层只订 战斗结算 的 ADR 纪律下唯一合法来源)、
# ux_refresh_attempt / ux_window_choice / ux_reject / ux_hint_shown(组合根显式上报)。
# 诊断转储 = 全量核心状态 + 最近结算/操作环形缓冲 → user://logs/diag_<时间戳>.json(卡死时点按钮)。
class_name PlaytestLogger
@@ -20,7 +20,7 @@ var _seed := 0
var _recent_steps: Array = [] # 环形缓冲:最近结算行
var _recent_actions: Array = [] # 环形缓冲:最近玩家动作
var _snap_count := 0
var ap_per_round := 8 # 组合根注入(AP 消耗口径分母——A3 无推送信号)
var ap_per_round := 8 # 组合根注入(AP 消耗口径分母——行动经济 无推送信号)
var _ux_inputs := 0 # 本结算轮原始输入动作数(log_action 计数,快照后归零)
@@ -50,10 +50,10 @@ func log_path() -> String:
return _path
func attach_battle(b1) -> void:
b1.step_row.connect(_on_step_row)
b1.settlement_snapshot.connect(_on_settlement_snapshot)
b1.battle_finished.connect(func(p): _append({
func attach_battle(settlement) -> void:
settlement.step_row.connect(_on_step_row)
settlement.settlement_snapshot.connect(_on_settlement_snapshot)
settlement.battle_finished.connect(func(p): _append({
"kind": "battle_end", "result": String(p.get("result", "")),
"hero_hp": int(p.get("hero_hp", 0)), "remaining_items": int(p.get("remaining_items", 0))}))
@@ -4,7 +4,7 @@
# 〔pattern 7 破坏性确认异位:RAP−8 · 旧板旗作废显式代价〕)。
# Date: 2026-09-12
# 高内聚边界:纯视图——意图经信号外发(stop_pressed/refresh_confirmed/legend_toggled),
# 状态由组合根经 setter 注入;刷新可用性预判 = RAP≥8 读数(A3 无推送信号——每次调用返回后重查)。
# 状态由组合根经 setter 注入;刷新可用性预判 = RAP≥8 读数(行动经济 无推送信号——每次调用返回后重查)。
class_name ApBarView
extends PanelContainer
@@ -92,7 +92,7 @@ func _init() -> void:
refresh_box.add_child(_refresh_sub)
# ---- setter 面(组合根每次核心调用返回后重查注入——A3 无推送信号) ----
# ---- setter 面(组合根每次核心调用返回后重查注入——行动经济 无推送信号) ----
func set_ap(ap: int, ap_max: int) -> void:
while _dots.get_child_count() < ap_max:
@@ -58,7 +58,7 @@ func _init() -> void:
visible = false
# 终局画面数据锚 = B1 判定行(verdict)——判定行文案由组合根装配传入
# 终局画面数据锚 = 战斗结算 判定行(verdict)——判定行文案由组合根装配传入
func open_victory(verdict_line: String, continue_label: String, board_rect: Rect2) -> void:
_dim.color = Color(COL_WARM, 0.28)
_verdict.text = verdict_line
@@ -3,8 +3,9 @@
# 实时流 / 点击展开回看 overlay〔pattern 6 模态纪律〕;行模板「{行为} {值} → {目标}(来源)」、
# 聚合计数行、零值行可见、效果行 value≠pool_value 禁相加、未生效四类行 icon/划线形状冗余、
# highlight 金框〔flag_hit/clear_reward_h2/perfect_broken〕、击杀标记)。
# Date: 2026-09-12(基础 = 2026-09-11 13 类行协议格式化版)
# 高内聚边界:B1 步骤流 → 玩家文案的全部格式化规则内聚于此;来源文案映射来自 SettlementTable。
# Date: 2026-09-12(基础 = 2026-09-11 13 类行协议格式化版;2026-09-15 窗口锚行批 13→15 类:
# 新增 window_open / window_skip 两类锚行格式化——战斗结算 行协议清单同步)
# 高内聚边界:战斗结算 步骤流 → 玩家文案的全部格式化规则内聚于此;来源文案映射来自 SettlementTable。
class_name BattleLogView
extends PanelContainer
@@ -162,7 +163,7 @@ func _format_row(row: Dictionary, agg_count := -1) -> String:
var suffix := "(%s)" % src if src != "" else ""
match String(row.get("type", "")):
"effect":
# 效果行 value = 单次贡献值,pool_value = 池合成值——禁相加呈现(B1 呈现细则)
# 效果行 value = 单次贡献值,pool_value = 池合成值——禁相加呈现(战斗结算 呈现细则)
var pool: int = int(row.get("pool_value", 0))
var col := "#7cc47c" if v > 0 else "#c47c7c"
var body := "%s 单次%+d(池 %+d)%s" % [name_, v, pool, suffix]
@@ -199,6 +200,10 @@ func _format_row(row: Dictionary, agg_count := -1) -> String:
return "[color=#e0b860][b]◆ %s[/b][/color]" % name_
"highlight":
return prefix + _gold_frame("★ %s" % name_)
"window_open":
return prefix + "[color=#c8b070]▤ %s[/color]" % name_
"window_skip":
return prefix + "[color=#9a8a72]▤ %s[/color]" % name_
return prefix + name_ + suffix
@@ -1,6 +1,6 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 实现 GDD:design/ux/battle.md(D 区演出层——清图金粒喷泉+彩纸 3–5s 占位、Boss 紫赤影子爬棋盘
# 一次性占位、完美排雷金光带+横幅占位、标中旗加重弹道占位;全部程序化色板原语,真实美术归 B5)。
# 一次性占位、完美排雷金光带+横幅占位、标中旗加重弹道占位;全部程序化色板原语,真实美术归 战斗表现)。
# Date: 2026-09-12
# 高内聚边界:纯演出——组合根按核心信号调用 play_*;本类零核心依赖、不自持游戏状态。
class_name BoardFxView
@@ -1,11 +1,11 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 实现 GDD:design/ux/battle.md(战斗屏 D 区 BoardView——四格态/悬停预承诺光标/可挖域呼吸微光/
# BFS 路径预览/奖励徽章/雾区全开一次性完成信号/结算期去饱和锁输入)。
# Date: 2026-09-12(battle.md 五区改造批;基础 = 2026-09-11 D1-lite 版)
# Date: 2026-09-12(battle.md 五区改造批;基础 = 2026-09-11 HUD框架-lite 版)
# 棋盘视图:格子渲染(art-bible 明亮风、数字高对比、雾形状+色相双通道、
# 绘制优先级 数字>小人>旗>奖励徽章>变异纹带>底色)+ 玩家输入意图翻译 + 悬停预承诺三态。
# 高内聚边界:只读 A2 查询面(+B4 身份解析回调)做渲染与意图识别,动作经信号外发——不调用核心写接口
# (settle_batch 归组合根编排),不持久持有战斗态(a2 引用随 refresh 更新,仅作只读查询)。
# 高内聚边界:只读 排雷玩法 查询面(+奖励道具经济 身份解析回调)做渲染与意图识别,动作经信号外发——不调用核心写接口
# (settle_batch 归组合根编排),不持久持有战斗态(minesweeper 引用随 refresh 更新,仅作只读查询)。
# 缺陷①回归约束:格子控件常驻、逐格重绘在 refresh 内完成,无布局回流(格控件/子标记定长,不增删)。
# ADR-0003 形态约束:GridContainer + 逐格常驻控件(探针耦合 _cells/click_cell——禁盲目迁移 _draw)。
class_name BoardView
@@ -30,6 +30,7 @@ const COL_HL_TARGET := Color8(240, 168, 40) # 悬停目标格边框(金)
const REWARD_BADGE := { # 已开格奖励身份 icon(tier 档位另经字号/形状区分)
"instant_aoe": "爆", "instant_heal": "急", "damage_item": "刃", "heal_pack": "医", "buff": "纹",
}
const BADGE_PLACEHOLDER := "?" # 未知图标键占位纹样(HUD框架:禁静默空图标)
const CURSOR_DIG := Input.CURSOR_CROSS # 挖——镐形自绘纹理载体
const CURSOR_FLAG := Input.CURSOR_POINTING_HAND # 旗——旗形自绘纹理载体
const CURSOR_MOVE_SEM := Input.CURSOR_ARROW # 移动——箭头(命名避开 Control.CURSOR_MOVE 原生成员遮蔽)
@@ -40,8 +41,8 @@ var _cells := {} # Vector2i → Button
var _cell_sb := {} # Vector2i → StyleBoxFlat(呼吸微光就地改边框色)
var _marker := {} # Vector2i → {"dot": ColorRect, "badge": Label, "hl": Panel}
var _board: BoardData = null
var _a2 = null # A2 只读查询引用(refresh 更新;null = 无战斗)
var _identity_cb := Callable() # B4 身份解析回调(组合根注入只读面)——Vector2i → Dictionary/null
var _minesweeper = null # 排雷玩法 只读查询引用(refresh 更新;null = 无战斗)
var _identity_cb := Callable() # 奖励道具经济 身份解析回调(组合根注入只读面)——Vector2i → Dictionary/null
var _diggable := {} # 可挖域(8 向相邻未开格)——呼吸微光作用集
var _breath_t := 0.0
var _dig_glow := false # 挖掘期才允许唯一在动元素(art-bible §2.1)
@@ -49,7 +50,7 @@ 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₀(B4 mapping 资格域 = S∖R₀——徽章查询跳过,防 E19 诊断噪音)
var _r0 := {} # 出生展开区 R₀(奖励道具经济 mapping 资格域 = S∖R₀——徽章查询跳过,防 E19 诊断噪音)
var _desat_mat: ShaderMaterial
var _desat_tween: Tween
var _cell := 46.0 # 当前格边长(set_cell_area 按容器尺寸重算——battle.md 格尺寸公式)
@@ -91,7 +92,7 @@ func set_desaturated(on: bool) -> void:
(_cells[c] as Button).material = _desat_mat if on else null
# 组合根注入 B4 身份解析只读回调(query_identity)——奖励徽章数据源
# 组合根注入 奖励道具经济 身份解析只读回调(query_identity)——奖励徽章数据源
func set_identity_resolver(cb: Callable) -> void:
_identity_cb = cb
@@ -107,10 +108,10 @@ func set_cell_area(avail: Vector2) -> void:
(_cells[c] as Button).custom_minimum_size = Vector2(_cell, _cell)
# 战斗启动/换板:按板尺寸重建格子控件(a2 引用随之更新)
func rebuild(board: BoardData, a2) -> void:
# 战斗启动/换板:按板尺寸重建格子控件(minesweeper 引用随之更新)
func rebuild(board: BoardData, minesweeper) -> void:
_board = board
_a2 = a2
_minesweeper = minesweeper
for child in _grid.get_children():
child.queue_free()
_cells.clear()
@@ -144,12 +145,12 @@ func rebuild(board: BoardData, a2) -> void:
# 逐格重绘(挖掘/结算后调用)
func refresh() -> void:
if _board == null or _a2 == null:
if _board == null or _minesweeper == null:
return
var hero: Vector2i = _a2.hero_pos()
var hero: Vector2i = _minesweeper.hero_pos()
_diggable.clear()
for n in _board.neighbors8(hero):
if not _a2.is_revealed(n):
if not _minesweeper.is_revealed(n):
_diggable[n] = true
for y in _board.height:
for x in _board.width:
@@ -167,16 +168,16 @@ func click_cell(cell: Vector2i, button: int) -> void:
func _on_cell_input(c: Vector2i, ev: InputEvent) -> void:
if _a2 == null or not (ev is InputEventMouseButton and ev.pressed):
if _minesweeper == null or not (ev is InputEventMouseButton and ev.pressed):
return
if ev.button_index == MOUSE_BUTTON_LEFT:
if _a2.is_revealed(c):
if _minesweeper.is_revealed(c):
action_requested.emit(c, "move")
else:
action_requested.emit(c, "dig")
_detect_chord(c)
elif ev.button_index == MOUSE_BUTTON_RIGHT:
if _a2.has_flag(c):
if _minesweeper.has_flag(c):
action_requested.emit(c, "flag_pull")
else:
action_requested.emit(c, "flag_place")
@@ -212,11 +213,11 @@ func _refresh_hover_after_paint() -> void:
func _apply_hover_visuals(c: Vector2i) -> void:
if _a2 == null or not _cells.has(c):
if _minesweeper == null or not _cells.has(c):
return
_clear_hover_visuals(c)
var revealed: bool = _a2.is_revealed(c)
var flag: bool = _a2.has_flag(c)
var revealed: bool = _minesweeper.is_revealed(c)
var flag: bool = _minesweeper.has_flag(c)
var adjacent: bool = _diggable.has(c)
var hl: Panel = _marker[c]["hl"]
if revealed:
@@ -253,9 +254,9 @@ func _set_cursor_shape(c: Vector2i, shape: int) -> void:
func _show_path_preview(c: Vector2i) -> void:
# D3 合法性指示:悬停已开格 → BFS 路径蓝细线点迹(compute_move_path 只读面)
if _a2 == null:
if _minesweeper == null:
return
var path: Array = _a2.compute_move_path(c)
var path: Array = _minesweeper.compute_move_path(c)
for pc in path:
if _marker.has(pc):
_marker[pc]["dot"].visible = true
@@ -271,36 +272,45 @@ func _hide_path_preview() -> void:
func _update_badge(c: Vector2i, btn: Button) -> void:
var badge: Label = _marker[c]["badge"]
badge.visible = false
if _a2 == null or not _a2.is_revealed(c) or not _identity_cb.is_valid():
if _minesweeper == null or not _minesweeper.is_revealed(c) or not _identity_cb.is_valid():
return
if _r0.has(c):
return # R₀ 初始展开格无奖励身份(B4 资格域 = S∖R₀)
var rec: Dictionary = _a2.reveal_record(c) # E2 收口:已开记录经查询面(禁直读 A2 状态字典)
return # R₀ 初始展开格无奖励身份(奖励道具经济 资格域 = S∖R₀)
var rec: Dictionary = _minesweeper.reveal_record(c) # E2 收口:已开记录经查询面(禁直读 排雷玩法 状态字典)
if rec.get("kind", "") != "safe":
return
var ident = _identity_cb.call(c)
if not (ident is Dictionary):
return
badge.text = String(REWARD_BADGE.get(String(ident.get("category", "")), "◇"))
badge.text = _badge_glyph(String(ident.get("category", "")))
badge.add_theme_font_size_override("font_size", maxi(10, int(_cell * 0.28)))
badge.visible = true
# 奖励图标键解析(HUD框架:查不到的键立即断言 + ? 占位纹样 + 诊断;键集 = 奖励道具经济 category 五值枚举)
func _badge_glyph(category: String) -> String:
if REWARD_BADGE.has(category):
return REWARD_BADGE[category]
push_error("[棋盘] 未知奖励图标键(映射表外 category——立即断言): %s" % category)
assert(false, "未知奖励图标键: " + category)
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 = _a2.is_revealed(c)
var revealed: bool = _minesweeper.is_revealed(c)
var fog: bool = _board.variant_ids(c).size() > 0
var flag: bool = _a2.has_flag(c)
var flag: bool = _minesweeper.has_flag(c)
var is_hero: bool = c == hero
var text := ""
var bg: Color
var digit_col := COL_DIGIT
if revealed:
var rec: Dictionary = _a2.reveal_record(c) # E2 收口:已开记录经查询面
var rec: Dictionary = _minesweeper.reveal_record(c) # E2 收口:已开记录经查询面
if rec.get("kind", "") == "mine":
bg = COL_FLOOR
text = "雷"
@@ -415,7 +425,7 @@ func _check_fog_completion() -> void:
continue
var all_open := true
for c in comp:
if not _a2.is_revealed(c):
if not _minesweeper.is_revealed(c):
all_open = false
break
if all_open:
@@ -1,11 +1,11 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 实现 GDD:design/ux/battle.md(B 区敌人卡 ×1–3——敌像占位、血条、顺位徽标、意图徽标〔常驻:
# 挖掘期全程可见,映射 = enemy_id 恒定、环点 = 拍数直觉编码,B2 数据源 = query_beat 当前拍〕、
# 状态槽〔敌方 side 池口径:伏击层数全体共享单值各卡同值——B1 流水经 B3 投影〕)。
# 挖掘期全程可见,映射 = enemy_id 恒定、环点 = 拍数直觉编码,敌人系统 数据源 = query_beat 当前拍〕、
# 状态槽〔敌方 side 池口径:伏击层数全体共享单值各卡同值——战斗结算 流水经 增益状态 投影〕)。
# Date: 2026-09-12
# 敌人死亡呈现(B2 资产下限同等 blocking):血条归零 → 卡体灰化 + 碎裂退场,禁瞬消;
# 敌人死亡呈现(敌人系统 资产下限同等 blocking):血条归零 → 卡体灰化 + 碎裂退场,禁瞬消;
# 卡体保留灰化残位——定长卡槽,死亡动画不回流布局。
# 意图线索映射目录(B2×4① 成文占位——资产定稿归 B5,本处按 enemy_id 程序化占位)。
# 意图线索映射目录(敌人系统×4① 成文占位——资产定稿归 战斗表现,本处按 enemy_id 程序化占位)。
class_name EnemyCardView
extends PanelContainer
@@ -65,7 +65,7 @@ func _init() -> void:
col.add_child(_status_label)
# data = B3/B2 投影:{order, display_name, hp, max_hp, enemy_id, beat, ring_size, ring_index, ambush_layers}
# data = 增益状态/敌人系统 投影:{order, display_name, hp, max_hp, enemy_id, beat, ring_size, ring_index, ambush_layers}
func set_data(d: Dictionary) -> void:
_name_label.text = "%s" % String(d.get("display_name", "敌人"))
_hp_bar.max_value = maxi(1, int(d.get("max_hp", 1)))
@@ -81,12 +81,12 @@ func set_data(d: Dictionary) -> void:
func _set_order(i: int) -> void:
# 顺位徽标 = 行主序生成序(固定顺位——B2 顺位标记义务)
# 顺位徽标 = 行主序生成序(固定顺位——敌人系统 顺位标记义务)
var base: String = _name_label.text.trim_prefix("① ").trim_prefix("② ").trim_prefix("③ ")
_name_label.text = "%s %s" % [String("①②③"[clampi(i - 1, 0, 2)]), base]
# beat = B2 行为环词表原键(attack/charge/heavy,B1 query_beat 直出——presenter 不翻译);
# beat = 敌人系统 行为环词表原键(attack/charge/heavy,战斗结算 query_beat 直出——presenter 不翻译);
# 名(BEAT_NAMES)/色(COL_BEAT)/重击标记(▼)渲染映射唯一定义处 = 本类(code-review 2026-09-12:
# 双翻译曾使色相与重击通道永久失效)。信号/入参契约:set_data 每结算行刷新、set_intent 每拍刷新。
func set_intent(beat: String, ring_size: int, ring_index: int) -> void:
@@ -151,7 +151,7 @@ func _revive() -> void:
func _portrait_glyph(enemy_id: String) -> String:
# enemy_id 恒定映射(B2 Rule 8③)——程序化占位字形:小体量墩形/垂直延伸/最大体量
# enemy_id 恒定映射(敌人系统 Rule 8③)——程序化占位字形:小体量墩形/垂直延伸/最大体量
match enemy_id:
"goblin":
return "▂"
@@ -68,7 +68,7 @@ func set_idle() -> void:
_status_label.text = ""
# battle_over 反馈(B3×4④):状态逐槽化光收回 + 微文案——英雄卡侧紧凑引用同步
# battle_over 反馈(增益状态×4④):状态逐槽化光收回 + 微文案——英雄卡侧紧凑引用同步
func set_status_cleared_feedback() -> void:
_status_label.text = "状态随战斗结束消散"
@@ -1,6 +1,6 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 实现 GDD:design/ux/battle.md(C 区角色信息 240×520——HP/攻击(+修正)/RAP 直角细线数据框
# 〔修正来源角标〕、状态效果槽 ×4、小人立绘占位;RAP 清零可见反馈〔A3 义务〕;
# 〔修正来源角标〕、状态效果槽 ×4、小人立绘占位;RAP 清零可见反馈〔行动经济 义务〕;
# 状态槽空态 = 槽隐藏上移补位〔EC12〕)。
# Date: 2026-09-12
# 高内聚边界:纯视图 setter 面——装配(含修正文案)归 hud_presenter.gd;定长槽 + clip_contents
@@ -70,7 +70,7 @@ func _init() -> void:
_status_feedback.add_theme_color_override("font_color", Color8(140, 124, 100))
col.add_child(_status_feedback)
# 小人立绘占位(程序化色块——真实立绘归 B5)
# 小人立绘占位(程序化色块——真实立绘归 战斗表现)
var portrait := ColorRect.new()
portrait.color = Color8(46, 110, 142, 56)
portrait.custom_minimum_size = Vector2(0, 120)
@@ -92,7 +92,7 @@ func set_hero(hp: int, max_hp: int) -> void:
func set_attack(atk: int, mod_text: String) -> void:
# 修正来源角标:mod_text 形如「(+50% 增伤)」——来源 = 效果 display_name(B3 投影)
# 修正来源角标:mod_text 形如「(+50% 增伤)」——来源 = 效果 display_name(增益状态 投影)
_atk_label.text = "攻击 %d %s" % [atk, mod_text]
@@ -102,7 +102,7 @@ func set_rap(rap: int, cap_hint: int) -> void:
_rap_label.text = "RAP %d(攒 %d 刷图)" % [rap, cap_hint]
_render_rap_dots(rap)
if prev > 0 and rap == 0:
# battle_over RAP 清零可见反馈(A3 D1 义务):数字滚动归零 + 微文案占位
# battle_over RAP 清零可见反馈(行动经济 D1 义务):数字滚动归零 + 微文案占位
_rap_feedback.text = "随战斗结束消散"
var tw := create_tween()
tw.tween_property(_rap_label, "modulate", Color(1.4, 1.2, 0.7), 0.12)
@@ -116,7 +116,7 @@ func set_status_effects(effects: Array) -> void:
_status_feedback.text = ""
# battle_over:状态槽全清可见反馈(逐槽化光 + 微文案——B3×4④)
# battle_over:状态槽全清可见反馈(逐槽化光 + 微文案——增益状态×4④)
func set_status_cleared_feedback() -> void:
slot_row.set_cleared_feedback()
_status_feedback.text = "状态随战斗结束消散"
@@ -4,7 +4,7 @@
# 〔布局决策 D2 / pattern 2 原位窗口激活〕:描边金光点亮、标题切换+时机指示、usable_now 禁选态
# +原因〔pattern 8 禁用态即理由〕、跳过按钮、政策注记常驻)。
# Date: 2026-09-12(改造自 item_window_overlay.gd——中央模态形态废弃,原位激活替代)
# 契约红线:选择语义 = 窗口条目位置(B1 义务④"选定序"),库存句柄不出本类
# 契约红线:选择语义 = 窗口条目位置(战斗结算 义务④"选定序"),库存句柄不出本类
# (缺陷④契约约束的呈现侧收口点)。来源 {cell, round} 不呈现于列表与 tooltip(2026-09-12 拍板)。
class_name InventoryPanelView
extends PanelContainer
@@ -63,7 +63,7 @@ func _init() -> void:
col.add_child(_policy)
# ---- 常读态(挖掘期规划面——B4 D1 义务①) ----
# ---- 常读态(挖掘期规划面——奖励道具经济 D1 义务①) ----
func show_inventory(rows: Array) -> void:
if Time.get_ticks_msec() < _cleared_hold_until:
@@ -76,7 +76,7 @@ func show_inventory(rows: Array) -> void:
_rebuild_rows(rows, false)
# 空态(指引性,非报错——B4 EC 空态)
# 空态(指引性,非报错——奖励道具经济 EC 空态)
func show_empty() -> void:
if Time.get_ticks_msec() < _cleared_hold_until:
return
@@ -86,7 +86,7 @@ func show_empty() -> void:
# ---- 窗口态(pattern 2 原位激活:面板描边金光点亮、标题切换 + 时机指示常驻) ----
# items = B1 组装窗口投影(usable_now 由 B1 组装——呈现层只消费不推断)
# items = 战斗结算 组装窗口投影(usable_now 由 战斗结算 组装——呈现层只消费不推断)
func open_window(items: Array) -> void:
_window_mode = true
_set_window_visual(true)
@@ -110,7 +110,7 @@ func is_open() -> bool:
return _window_mode
# 库存清空反馈(B4 D1×⑤):逐件化光点飞出 + 「剩余道具 ×N 随关卡结束清空」占位
# 库存清空反馈(奖励道具经济 D1×⑤):逐件化光点飞出 + 「剩余道具 ×N 随关卡结束清空」占位
func play_cleared_feedback(remaining: int) -> void:
_cleared_hold_until = Time.get_ticks_msec() + 3000
var rows := _list.get_children()
@@ -1,6 +1,6 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 实现 GDD:design/ux/battle.md(D 区图例按钮 + 模态 overlay——格态/变异/雾语义图例,
# 「迷雾 = 无雷盲区」条目恒在〔A4 D1×②〕;状态槽空态说明成文载入〔B3 D1×①〕)。
# 「迷雾 = 无雷盲区」条目恒在〔地形变异 D1×②〕;状态槽空态说明成文载入〔增益状态 D1×①〕)。
# Date: 2026-09-12
# pattern 6 模态 overlay 层级纪律:打开时由组合根隐藏上方遮挡层 + 锁棋盘输入;
# 本类自持开关(toggle),探针断言"打开后自身 visible"——遮挡物断言归场景探针。
@@ -41,7 +41,7 @@ func _init() -> void:
box.add_theme_constant_override("separation", 6)
_panel.add_child(box)
box.add_child(UiKit.label("图例 · 格态与变异", 20, true))
# 「迷雾 = 无雷盲区」条目恒在(A4 图例条目恒在义务)
# 「迷雾 = 无雷盲区」条目恒在(地形变异 图例条目恒在义务)
box.add_child(UiKit.label("迷雾 = 无雷盲区:雾格不藏雷,不需要赌命开雾。", 14, true))
box.add_child(HSeparator.new())
for e in ENTRIES:
@@ -1,7 +1,7 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 实现 GDD:design/ux/battle.md(A 区关卡进度条——全宽 × 28px 顶栏,当前节点高亮、已过节点暗金)。
# Date: 2026-09-12(battle.md 五区改造批——对齐 28px 顶栏与高亮样式;基础 = 2026-09-11 C1-lite 呈现)
# 关卡节点进度条视图(C1-lite 呈现):线性节点链 战→赏→疗→王 的推进状态。
# Date: 2026-09-12(battle.md 五区改造批——对齐 28px 顶栏与高亮样式;基础 = 2026-09-11 关卡节点地图-lite 呈现)
# 关卡节点进度条视图(关卡节点地图-lite 呈现):线性节点链 战→赏→疗→王 的推进状态。
# 高内聚边界:只读 runner 的节点链/进度做渲染,无任何写面。
class_name LevelStripView
extends HBoxContainer
@@ -1,29 +1,37 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 实现 GDD:design/ux/battle.md(C 区状态效果槽 ×4〔B3 D1×①〕——effect_id→icon 形状+色相双通道、
# 实现 GDD:design/ux/battle.md(C 区状态效果槽 ×4〔增益状态 D1×①〕——effect_id→icon 形状+色相双通道、
# 层数角标、剩余结算次数点、悬停 tooltip(来源+语义+剩余);空态 = 槽隐藏(数据框上移补位,
# EC12 空态裁决);极性映射 = B1 SettlementTable 效果语义而非 magnitude 符号〔B3 D1×③〕)。
# EC12 空态裁决);极性映射 = 战斗结算 SettlementTable 效果语义而非 magnitude 符号〔增益状态 D1×③〕)。
# Date: 2026-09-12
# 高内聚边界:纯视图——效果投影装配归 hud_presenter.gd;B3 纯被动容器零信号——刷新绑 B1
# 高内聚边界:纯视图——效果投影装配归 hud_presenter.gd;增益状态 纯被动容器零信号——刷新绑 战斗结算
# 结算编排完成时点(battle.md 更新触发注记)。
class_name StatusSlotRow
extends HBoxContainer
const MAX_SLOTS := 4
# 极性/语义映射(数据源 = SettlementTable.effect_pool_map 效果语义 + StatusEffectTable display_name):
# 益 = ▲/◆/▼(金/蓝族);害 = ⚡(旗朱族)——形状+色相双通道,文字名常驻第三通道
# 益 = ▲/◆/▼(金/蓝族);害 = ⚡/⚔/✕(旗朱族)——形状+色相双通道,文字名常驻第三通道。
# 状态表六行全映射(attack_up/defense_up/weaken/ambush/enrage/off_guard)——HUD框架 图标键契约
# "一键一枚专属纹样,查不到的键立即断言 + ? 占位 + 诊断"(2026-09-15 补暴走/失衡两键)。
const EFFECT_ICON := {
"attack_up": ["▲", Color8(176, 122, 20), "益"],
"defense_up": ["◆", Color8(46, 110, 142), "益"],
"weaken": ["▼", Color8(46, 110, 142), "益"],
"ambush": ["⚡", Color8(198, 61, 34), "害"],
"enrage": ["⚔", Color8(198, 61, 34), "害"],
"off_guard": ["✕", Color8(198, 61, 34), "害"],
}
const EFFECT_SEMANTIC := {
"attack_up": "主角攻击加成(hero_offense 池加算)",
"defense_up": "受到伤害减免(hero_defense 池加算)",
"weaken": "敌方输出削弱(enemy_output 池乘算)",
"ambush": "敌方全体攻击附加(flat_attach——踩雷转化,战斗段共享不消耗)",
"enrage": "敌方输出加成(enemy_output 池加算——标错反噬)",
"off_guard": "主角承伤加深(hero_defense 池加算——标错反噬)",
}
const SOURCE_NAMES := {"safe_cell": "安全格", "flag_hit": "标中旗", "mine_hit": "踩雷", "clear_reward": "清图奖励"}
# 未知图标键的占位纹样(HUD框架:占位也要可见,禁静默空图标)
const ICON_PLACEHOLDER := ["?", Color8(120, 106, 88), "?"]
var _tip: PanelContainer
var _tip_label: Label
@@ -54,7 +62,7 @@ func _init() -> void:
_tip.add_child(_tip_label)
# effects = B3 query_active 投影数组(含空态);空槽隐藏,非空按插入序填充(最多 4 槽可见——超出截断)
# effects = 增益状态 query_active 投影数组(含空态);空槽隐藏,非空按插入序填充(最多 4 槽可见——超出截断)
func set_effects(effects: Array) -> void:
_cleared_feedback = false
_sync_slots(effects.size())
@@ -63,7 +71,7 @@ func set_effects(effects: Array) -> void:
_slots[i].visible = true
# battle_over 反馈(B3×4④):逐槽化光收回 + 微文案(下次 set_effects 恢复常态)
# battle_over 反馈(增益状态×4④):逐槽化光收回 + 微文案(下次 set_effects 恢复常态)
func set_cleared_feedback() -> void:
_cleared_feedback = true
for slot in _slots:
@@ -116,7 +124,7 @@ func _fill_slot(slot: PanelContainer, eff: Dictionary) -> void:
var col := slot.get_child(0) as VBoxContainer
var icon := col.get_child(0) as Label
var meta := col.get_child(1) as Label
var icon_cfg: Array = EFFECT_ICON.get(String(eff.effect_id), ["◇", Color8(120, 106, 88), "?"])
var icon_cfg: Array = _icon_cfg(String(eff.effect_id))
icon.text = String(icon_cfg[0])
icon.add_theme_color_override("font_color", icon_cfg[1])
# 层数角标 + 剩余结算次数点(● 未耗余量)
@@ -132,7 +140,7 @@ func _on_slot_hover(slot: Control) -> void:
if not slot.has_meta("eff"):
return
var eff: Dictionary = slot.get_meta("eff")
var icon_cfg: Array = EFFECT_ICON.get(String(eff.effect_id), ["◇", Color8(120, 106, 88), "?"])
var icon_cfg: Array = _icon_cfg(String(eff.effect_id))
var src: String = String(SOURCE_NAMES.get(String(eff.get("source_class", "")), String(eff.get("source_class", ""))))
var semantic: String = String(EFFECT_SEMANTIC.get(String(eff.effect_id), String(eff.get("display_name", ""))))
_tip_label.text = "%s〔%s〕\n%s\n剩余结算 %d 次\n来源:%s(第 %d 轮)" % [
@@ -142,3 +150,12 @@ func _on_slot_hover(slot: Control) -> void:
add_child(_tip)
_tip.position = Vector2(0, -70)
_tip.visible = true
# 图标键解析(HUD框架:查不到的键按错误处理——立即断言 + ? 占位纹样 + 诊断,禁止静默显示空图标)
func _icon_cfg(effect_id: String) -> Array:
if EFFECT_ICON.has(effect_id):
return EFFECT_ICON[effect_id]
push_error("[状态槽] 未知图标键(映射表外键——立即断言): %s" % effect_id)
assert(false, "未知图标键: " + effect_id)
return ICON_PLACEHOLDER
@@ -1,6 +1,6 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Date: 2026-09-11(2026-09-12 code-review 修复批重构)
# 测试用角色数值调整板(模拟天赋树属性提升——仅测试,正式侧归 C4 元进度)。
# 测试用角色数值调整板(模拟天赋树属性提升——仅测试,正式侧归元进度)。
# 纯视图边界(规范 1.3):本类只承载滑杆 UI 与生效值预览;加成应用 / 重置落表 / 批量模拟编排
# 全部经信号外发由组合根承接(apply_requested / reset_requested / simulate_requested / closed),
# 模拟进度与报告经 setter 注入——视图零核心写、零 smoke 脚本依赖、不拼报告文案(装配归 presenter)。
@@ -3,32 +3,32 @@
# 自动玩家策略集(headless 冒烟/扫描/转储共用的理性玩家近似):
# choose_dig = 挖最近可挖格(先移到邻接已开格);window_selection = 道具窗口确定性选择
# (治疗包优先于刀;半血阈值/轮次门/刀力口径经参数表达——四调用点原语义逐一保留)。
# 读取面:B1.presentation_snapshot() / A2 查询面(E1/E2 收口后的成文 API——禁直读核心状态)。
# 读取面:战斗结算.presentation_snapshot() / 排雷玩法 查询面(E1/E2 收口后的成文 API——禁直读核心状态)。
# 等价性证明:smoke_all 同种子回放断言(步骤流逐行相同)在重构后保持全绿。
extends RefCounted
# 行主序枚举未开格,找与已开集 8 邻接者;小人先移到该邻接已开格(BFS 可达)。无可挖 → {}
static func choose_dig(a2) -> Dictionary:
var revealed: Array = a2.revealed_cells()
static func choose_dig(minesweeper) -> Dictionary:
var revealed: Array = minesweeper.revealed_cells()
var revealed_set := {}
for c in revealed:
revealed_set[c] = true
var board = a2.current_board()
var board = minesweeper.current_board()
for y in board.height:
for x in board.width:
var c := Vector2i(x, y)
if a2.is_revealed(c) or a2.has_flag(c):
if minesweeper.is_revealed(c) or minesweeper.has_flag(c):
continue
for n in board.neighbors8(c):
if revealed_set.has(n):
var path: Array = a2.compute_move_path(n)
if not path.is_empty() or n == a2.hero_pos():
var path: Array = minesweeper.compute_move_path(n)
if not path.is_empty() or n == minesweeper.hero_pos():
return {"dig_cell": c, "hero_cell": n}
return {}
# 道具窗口确定性选择(s = b1.presentation_snapshot();半血以下治疗包优先,否则刀):
# 道具窗口确定性选择(s = settlement.presentation_snapshot();半血以下治疗包优先,否则刀):
# opts.heal_hp_div = 治疗包启用分母(hero_hp × div ≤ max_hp——原四实现 2/3 两档)
# opts.round_gate = 刀启用轮次门(round ≥ gate 才用刀;0 = 不设门——monte_carlo 口径)
# opts.knife_compare = "best"(最强单刀 ≥ 敌余 HP)| "sum"(刀力合计——dump/monte 口径)
@@ -72,50 +72,50 @@ static func window_selection(s: Dictionary, items: Array, opts: Dictionary = {})
# ==================== 完美策略档(2026-09-14 用户拍板:OQ7 技能轴上界测量仪——只作测量,非玩法数值) ====================
# 策略语义:零盲挖(只挖已证安全格 ⇒ 构造保证零踩雷)· 已证雷格全标旗(旗先于挖——旗 = 纯收益
# 〔轮末 weaken + 伤害B 首次充能〕且雷上旗不阻连锁)· 卡死且刷新资源可用 → 轮内刷新
# (经 b1.player_request_refresh 核心编排)· 否则停手(残 AP 1:1 转存 RAP——下轮刷新资源)。
# 推理规则集 = A1 F1 measure_inferability 同族(single + subset——solver_rules=single_subset 冻结域);
# 口径差异 = unknown 集不含 A1 生成期 safe_set oracle(真实玩家约束的未知邻格含雷格)——比生成期闸
# 保守,卡死率略高属口径诚实。输入只经 A2 查询面(reveal_record/is_revealed/has_flag/current_board
# (经 settlement.player_request_refresh 核心编排)· 否则停手(残 AP 1:1 转存 RAP——下轮刷新资源)。
# 推理规则集 = 棋盘生成 F1 measure_inferability 同族(single + subset——solver_rules=single_subset 冻结域);
# 口径差异 = unknown 集不含 棋盘生成 生成期 safe_set oracle(真实玩家约束的未知邻格含雷格)——比生成期闸
# 保守,卡死率略高属口径诚实。输入只经 排雷玩法 查询面(reveal_record/is_revealed/has_flag/current_board
# 的邻居与可达面),禁读棋盘雷区真值(测推理上界,非全知作弊)。
# 道具窗口:轮次门解除(round_gate=0)+ 治疗无浪费(heal_no_waste)。
# 待拍板(未实现):非卡死主动刷新、囤刀倾泻 vs 即用即耗、完美板目标加权。
# 单步决策入口(每动作一查——挖掘揭示新数字后重推理):move/dig/flag/refresh/stop 五值。
static func perfect_action(a2, b1) -> Dictionary:
var ded := _deduce(a2)
static func perfect_action(minesweeper, settlement) -> Dictionary:
var ded := _deduce(minesweeper)
for c in ded["mines"]:
if not a2.is_revealed(c) and not a2.has_flag(c):
return _plan_on(a2, c, "flag")
if not minesweeper.is_revealed(c) and not minesweeper.has_flag(c):
return _plan_on(minesweeper, c, "flag")
for c in ded["safe"]:
if not a2.is_revealed(c) and not a2.has_flag(c):
return _plan_on(a2, c, "dig")
if b1 != null and bool(b1.can_request_refresh()):
if not minesweeper.is_revealed(c) and not minesweeper.has_flag(c):
return _plan_on(minesweeper, c, "dig")
if settlement != null and bool(settlement.can_request_refresh()):
return {"kind": "refresh"}
return {"kind": "stop"}
# 目标格需英雄 8 邻——不可达先移到目标的已开 8 邻格(行主序首个可达者;同 choose_dig 寻路口径)
static func _plan_on(a2, c: Vector2i, kind: String) -> Dictionary:
var d := Vector2i(absi(c.x - a2.hero_pos().x), absi(c.y - a2.hero_pos().y))
static func _plan_on(minesweeper, c: Vector2i, kind: String) -> Dictionary:
var d := Vector2i(absi(c.x - minesweeper.hero_pos().x), absi(c.y - minesweeper.hero_pos().y))
if maxi(d.x, d.y) == 1:
return {"kind": kind, "cell": c}
var board = a2.current_board()
var board = minesweeper.current_board()
for n in board.neighbors8(c):
if a2.is_revealed(n) and (n == a2.hero_pos() or not a2.compute_move_path(n).is_empty()):
if minesweeper.is_revealed(n) and (n == minesweeper.hero_pos() or not minesweeper.compute_move_path(n).is_empty()):
return {"kind": "move", "cell": n}
return {"kind": "stop"} # 不可达(理论不可达——已证格必邻接已开区);防御性停手防死循环
# 推理不动点:single(约束余雷=0⇒未知全安全 / 余雷=未知数⇒全雷)+ subset(U_a ⊂ U_b ⇒ 差集约束)。
# 约束只依赖 revealed/flags/mines 推断集——mines 集增长才需重建约束(safe 标记不改正约约束)。
static func _deduce(a2) -> Dictionary:
var board = a2.current_board()
static func _deduce(minesweeper) -> Dictionary:
var board = minesweeper.current_board()
var safe := {}
var mines := {}
var changed := true
while changed:
changed = false
var cons := _constraints(a2, board, mines)
var cons := _constraints(minesweeper, board, mines)
for con in cons:
if int(con["mines"]) == 0:
for c in con["unknown"].keys():
@@ -127,7 +127,7 @@ static func _deduce(a2) -> Dictionary:
if not mines.has(c):
mines[c] = true
changed = true
var cons2 := _constraints(a2, board, mines)
var cons2 := _constraints(minesweeper, board, mines)
for i in cons2.size():
for j in range(i + 1, cons2.size()):
var der := _subset_derived(cons2[i], cons2[j])
@@ -148,10 +148,10 @@ static func _deduce(a2) -> Dictionary:
# 约束集 = 已开数字格:mines_left = digit − 已知雷旗(旗位 + 已证雷格),unknown = 未开无旗 8 邻。
# 已爆雷格(kind=mine)不带数字约束——其贡献已含在邻格 digit 内(数字恒真不变式 inv3)。
static func _constraints(a2, board, mines: Dictionary) -> Array:
static func _constraints(minesweeper, board, mines: Dictionary) -> Array:
var out: Array = []
for c in a2.revealed_cells():
var rec: Dictionary = a2.reveal_record(c)
for c in minesweeper.revealed_cells():
var rec: Dictionary = minesweeper.reveal_record(c)
if String(rec.get("kind", "")) != "safe":
continue
var d := int(rec.get("digit", -1))
@@ -160,9 +160,9 @@ static func _constraints(a2, board, mines: Dictionary) -> Array:
var unknown := {}
var known := 0
for n in board.neighbors8(c):
if a2.has_flag(n):
if minesweeper.has_flag(n):
known += 1
elif not a2.is_revealed(n):
elif not minesweeper.is_revealed(n):
if mines.has(n):
known += 1
else:
@@ -5,7 +5,7 @@
# H2 Boss/敌攻击链缺陷(bug——fresh 池下攻击拍仍零兑现)
# 快照盲区说明:settlement_snapshot 取在衰减段之后,时效 1 的 defense_up 当轮授予当轮衰减——
# 真人局 JSONL buffs[] 恒空 ≠ 池为空,本探针改从结算行(授予/续存 effect 行的 pool_value)重建战斗段时点池值。
# 观测面:全部读 B1 结算行 / 表值 / 呈现快照(零核心穿透);池归属/墙深/池界全读表(D2 Rule 1——零数值字面量)。
# 观测面:全部读战斗结算步骤行 / 表值 / 呈现快照(零核心穿透);池归属/墙深/池界全读表(数据配置 Rule 1——零数值字面量)。
# 模式(-- 后参数):boss [seed](默认 42:Run A 零挖掘 fresh 池 + Run B 自动挖掘墙行为)
# level [seed](全关遭遇回放 + 多种子 spawn 扫描——报告 Bug#1 证据面)
# 运行:Godot_console.exe --headless --path . --script res://smoke/damage_probe.gd -- boss 42
@@ -83,37 +83,37 @@ func _probe_boss(db, seed_val: int) -> void:
func _run_boss(db, seed_val: int, dig: bool) -> Dictionary:
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(seed_val)
var b1 = load("res://core/battle_orchestrator.gd").new(db, rng)
var settlement = load("res://core/battle_orchestrator.gd").new(db, rng)
var cap := {"rows": [], "snaps": [], "result": ""}
b1.step_row.connect(func(row): cap["rows"].append(row))
b1.settlement_snapshot.connect(func(s): cap["snaps"].append(s))
b1.battle_finished.connect(func(p): cap["result"] = String(p.get("result", "")))
b1.window_opened.connect(func(items):
var s: Dictionary = b1.presentation_snapshot()
b1.submit_window_selection(AutoPlayer.window_selection(s, items, {"heal_hp_div": 2, "round_gate": 3, "knife_compare": "best"})))
if not b1.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(db.hero.max_hp)}):
settlement.step_row.connect(func(row): cap["rows"].append(row))
settlement.settlement_snapshot.connect(func(s): cap["snaps"].append(s))
settlement.battle_finished.connect(func(p): cap["result"] = String(p.get("result", "")))
settlement.window_opened.connect(func(items):
var s: Dictionary = settlement.presentation_snapshot()
settlement.submit_window_selection(AutoPlayer.window_selection(s, items, {"heal_hp_div": 2, "round_gate": 3, "knife_compare": "best"})))
if not settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(db.hero.max_hp)}):
cap["result"] = "begin_failed"
return cap
var rounds := 0
while b1.battle_result == "" and rounds < 40:
while settlement.battle_result == "" and rounds < 40:
rounds += 1
var start_round: int = b1.round_index
var start_round: int = settlement.round_index
if dig:
var a2 = b1.a2
var minesweeper = settlement.minesweeper
var guard := 400
while b1.a3.phase == "Active" and b1.round_index == start_round and guard > 0:
while settlement.action_economy.phase == "Active" and settlement.round_index == start_round and guard > 0:
guard -= 1
var plan := AutoPlayer.choose_dig(a2)
var plan := AutoPlayer.choose_dig(minesweeper)
if plan.is_empty():
break
if plan.hero_cell != a2.hero_pos():
a2.settle_batch([{"action": "move", "cell": plan.hero_cell}])
if b1.a3.phase == "Active":
a2.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
if b1.a3.phase == "Active" and b1.round_index == start_round:
a2.settle_batch([{"action": "stop"}])
if plan.hero_cell != minesweeper.hero_pos():
minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}])
if settlement.action_economy.phase == "Active":
minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
if settlement.action_economy.phase == "Active" and settlement.round_index == start_round:
minesweeper.settle_batch([{"action": "stop"}])
else:
b1.a2.settle_batch([{"action": "stop"}])
settlement.minesweeper.settle_batch([{"action": "stop"}])
return cap
@@ -184,7 +184,7 @@ func _analyze_round(db, math, rows: Array, beats: Dictionary) -> Dictionary:
var pv := int(row.get("pool_value", 0))
last_pool[eid] = pv
grants.append("%s→池%d" % [String(row.get("display_name", "")), pv])
# 池归属表驱动(settlement.effect_pool_map),percent 池加算后 clamp(B1 F2 同口径)
# 池归属表驱动(settlement.effect_pool_map),percent 池加算后 clamp(战斗结算 F2 同口径)
var pools := {"hero_defense": 0, "enemy_output": 0, "hero_offense": 0, "flat_attach": 0}
for eid in last_pool:
var pool_name: String = String(db.settlement.effect_pool_map.get(eid, ""))
@@ -254,7 +254,7 @@ func _probe_level(db, seed_val: int) -> void:
all_ge_1 = false
if spawn2 != int(prediction.get(int(b.get("depth", -1)), -2)):
all_match = false
check(all_ge_1, "全部战斗 spawn ≥ 1(C3 Rule 8 区间下限——空遭遇在当前实现不可达)")
check(all_ge_1, "全部战斗 spawn ≥ 1(难度曲线与Boss Rule 8 区间下限——空遭遇在当前实现不可达)")
check(all_match, "spawn 与独立遭遇骰预测逐战斗一致(encounter 封闭流同种子可回放)")
var expect_battles := _battle_nodes_upto(db, 1, int(cap.get("end_node", -1)))
check(battles.size() == expect_battles, "战斗推进数与节点链一致(实测 %d/预期 %d——败北截断或全链跑通)" % [battles.size(), expect_battles])
@@ -309,10 +309,10 @@ func _run_level(db, seed_val: int) -> Dictionary:
var guard_battles := 20
while cap["result"] == "" and guard_battles > 0:
guard_battles -= 1
var b1 = runner.current_battle()
if b1 == null:
var settlement = runner.current_battle()
if settlement == null:
break
_drive_battle(runner, b1, cap)
_drive_battle(runner, settlement, cap)
return cap
@@ -332,11 +332,11 @@ func _battle_nodes_upto(db, level_index: int, node_idx: int) -> int:
func _on_level_window(items: Array, runner) -> void:
var b1 = runner.current_battle()
if b1 == null:
var settlement = runner.current_battle()
if settlement == null:
runner.submit_window_selection([])
return
var s: Dictionary = b1.presentation_snapshot()
var s: Dictionary = settlement.presentation_snapshot()
runner.submit_window_selection(AutoPlayer.window_selection(s, items, {"heal_hp_div": 2, "round_gate": 3, "knife_compare": "best"}))
@@ -351,33 +351,33 @@ func _on_level_node_entered(node: Dictionary, _info: Dictionary, runner, cap: Di
func _on_level_round_started(ri: int, runner, cap: Dictionary) -> void:
if ri != 1 or cap["battles"].is_empty():
return
var b1 = runner.current_battle()
if b1 == null:
var settlement = runner.current_battle()
if settlement == null:
return
var s: Dictionary = b1.presentation_snapshot()
var s: Dictionary = settlement.presentation_snapshot()
cap["battles"][cap["battles"].size() - 1]["spawn"] = s.enemies.size()
func _drive_battle(runner, b1, cap: Dictionary) -> void:
func _drive_battle(runner, settlement, cap: Dictionary) -> void:
var round_guard := 60
while cap["result"] == "" and runner.current_battle() == b1 and b1.battle_result == "" and round_guard > 0:
while cap["result"] == "" and runner.current_battle() == settlement and settlement.battle_result == "" and round_guard > 0:
round_guard -= 1
var start_round: int = b1.round_index
var a2 = b1.a2
var start_round: int = settlement.round_index
var minesweeper = settlement.minesweeper
var action_guard := 400
while runner.current_battle() == b1 and b1.battle_result == "" and b1.a3.phase == "Active" \
and b1.round_index == start_round and action_guard > 0:
while runner.current_battle() == settlement and settlement.battle_result == "" and settlement.action_economy.phase == "Active" \
and settlement.round_index == start_round and action_guard > 0:
action_guard -= 1
var plan := AutoPlayer.choose_dig(a2)
var plan := AutoPlayer.choose_dig(minesweeper)
if plan.is_empty():
break
if plan.hero_cell != a2.hero_pos():
a2.settle_batch([{"action": "move", "cell": plan.hero_cell}])
if b1.a3.phase != "Active" or b1.round_index != start_round:
if plan.hero_cell != minesweeper.hero_pos():
minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}])
if settlement.action_economy.phase != "Active" or settlement.round_index != start_round:
break
a2.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
if runner.current_battle() == b1 and b1.battle_result == "" and b1.a3.phase == "Active" and b1.round_index == start_round:
a2.settle_batch([{"action": "stop"}])
minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
if runner.current_battle() == settlement and settlement.battle_result == "" and settlement.action_economy.phase == "Active" and settlement.round_index == start_round:
minesweeper.settle_batch([{"action": "stop"}])
# 战斗判定摘要:round-1 victory 的战斗必须能从结算行分辨「首轮全灭」vs「空场」——
@@ -445,11 +445,11 @@ func _scan_spawns(db, seed0: int, n_seeds: int) -> Dictionary:
rng.set_master_seed(seed0 + i)
for depth in depths:
total += 1
var b1 = load("res://core/battle_orchestrator.gd").new(db, rng)
if not b1.begin_battle({"level_index": 1, "node_depth": int(depth), "hero_hp": int(db.hero.max_hp)}):
var settlement = load("res://core/battle_orchestrator.gd").new(db, rng)
if not settlement.begin_battle({"level_index": 1, "node_depth": int(depth), "hero_hp": int(db.hero.max_hp)}):
bad += 1
continue
var n: int = b1.presentation_snapshot().enemies.size()
var n: int = settlement.presentation_snapshot().enemies.size()
var row: Dictionary = db.resolve_enemy_rowset(1, int(depth))
var lo := int(row.get("unit_count", 1))
var hi := int(row.get("unit_count_max", lo))
@@ -12,43 +12,43 @@ func _initialize() -> void:
db.load_tables("res://data/tables", false)
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(42) # 种子 = 42;第二个命令行参数可覆盖(关卡 种子)
var b1 = load("res://core/battle_orchestrator.gd").new(db, rng)
var settlement = load("res://core/battle_orchestrator.gd").new(db, rng)
var stream: Array = []
b1.step_row.connect(func(row): stream.append(row))
b1.window_opened.connect(func(items):
var s: Dictionary = b1.presentation_snapshot() # E1 收口:经呈现快照面读英雄/敌况
settlement.step_row.connect(func(row): stream.append(row))
settlement.window_opened.connect(func(items):
var s: Dictionary = settlement.presentation_snapshot() # E1 收口:经呈现快照面读英雄/敌况
var selection: Array = AutoPlayer.window_selection(s, items,
{"heal_hp_div": 3, "round_gate": 3, "knife_compare": "sum"}) # O4:原 dump_battle 口径
b1.submit_window_selection(selection))
settlement.submit_window_selection(selection))
var level := 2
if OS.get_cmdline_user_args().size() > 0:
level = int(OS.get_cmdline_user_args()[0])
if OS.get_cmdline_user_args().size() > 1:
rng.set_master_seed(int(OS.get_cmdline_user_args()[1]))
b1.begin_battle({"level_index": level, "node_depth": 0, "hero_hp": db.hero.max_hp})
settlement.begin_battle({"level_index": level, "node_depth": 0, "hero_hp": db.hero.max_hp})
print("=== 棋盘摘要 L%d ===" % level)
var bd = b1.a2.current_board()
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])
var rounds := 0
while b1.battle_result == "" and rounds < 40:
while settlement.battle_result == "" and rounds < 40:
rounds += 1
var start_round: int = b1.round_index
var a2 = b1.a2
var a3 = b1.a3
var start_round: int = settlement.round_index
var minesweeper = settlement.minesweeper
var action_economy = settlement.action_economy
var guard := 400
while a3.phase == "Active" and b1.round_index == start_round and guard > 0:
while action_economy.phase == "Active" and settlement.round_index == start_round and guard > 0:
guard -= 1
var plan := AutoPlayer.choose_dig(a2)
var plan := AutoPlayer.choose_dig(minesweeper)
if plan.is_empty():
break
if plan.hero_cell != a2.hero_pos():
a2.settle_batch([{"action": "move", "cell": plan.hero_cell}])
if a3.phase == "Active":
a2.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
if a3.phase == "Active" and b1.round_index == start_round:
a2.settle_batch([{"action": "stop"}])
print("战斗结果: %s(%d 轮)" % [b1.battle_result, rounds])
if plan.hero_cell != minesweeper.hero_pos():
minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}])
if action_economy.phase == "Active":
minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
if action_economy.phase == "Active" and settlement.round_index == start_round:
minesweeper.settle_batch([{"action": "stop"}])
print("战斗结果: %s(%d 轮)" % [settlement.battle_result, rounds])
print("=== 结算步骤流(%d 行)===" % stream.size())
for row in stream:
print(JSON.stringify(row))
@@ -6,7 +6,7 @@ extends SceneTree
func _initialize() -> void:
var db = load("res://core/config_db.gd").new()
db.load_tables("res://data/tables", true)
var a1 = load("res://core/board_generator.gd").new()
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")
for d in [0.22, 0.25, 0.28, 0.30, 0.33]:
@@ -29,7 +29,7 @@ func _initialize() -> void:
var params := _params_from(row)
var rg := RandomNumberGenerator.new()
rg.seed = 1000 + s
var b = a1.generate(1, 0, rg, _db(db, row))
var b = board_generator.generate(1, 0, rg, _db(db, row))
if b == null:
continue
agg.m += b.mines.size()
@@ -45,10 +45,10 @@ static func run_level(db, seed_val: int, level_index: int, atk_pct: int, hp_pct:
var guard_battles := 20
while st.result == "" and guard_battles > 0:
guard_battles -= 1
var this_b1 = runner.current_battle()
if this_b1 == null:
break # level_finished 已发(b1 置 null)而信号尚未置 result 的防御
_autoplay_battle(runner, this_b1, st, logger)
var this_settlement = runner.current_battle()
if this_settlement == null:
break # level_finished 已发(settlement 置 null)而信号尚未置 result 的防御
_autoplay_battle(runner, this_settlement, st, logger)
if st.result == "":
st.result = "timeout"
st.timeout = true
@@ -71,8 +71,8 @@ static func _on_step_row(row: Dictionary, _runner, st: Dictionary, collect_rows:
static func _on_window_opened(items: Array, runner, st: Dictionary, logger) -> void:
var b1 = runner.current_battle()
var s: Dictionary = b1.presentation_snapshot() # E1 收口:经呈现快照面读英雄/敌况
var settlement = runner.current_battle()
var s: Dictionary = settlement.presentation_snapshot() # E1 收口:经呈现快照面读英雄/敌况
var opts := {"heal_hp_div": 2, "round_gate": 3, "knife_compare": "best"} # O4:原 autoplay 口径
if String(st.get("policy", "normal")) == "perfect":
opts = {"heal_hp_div": 2, "round_gate": 0, "knife_compare": "best", "heal_no_waste": true}
@@ -116,64 +116,64 @@ static func _on_level_finished(p: Dictionary, st: Dictionary, logger) -> void:
logger.log_level_end(p)
static func _autoplay_battle(runner, b1, st: Dictionary, logger) -> void:
static func _autoplay_battle(runner, settlement, st: Dictionary, logger) -> void:
if String(st.get("policy", "normal")) == "perfect":
_autoplay_battle_perfect(runner, b1, st, logger)
_autoplay_battle_perfect(runner, settlement, st, logger)
return
var round_guard := 60
while st.result == "" and runner.current_battle() == b1 and b1.battle_result == "" and round_guard > 0:
while st.result == "" and runner.current_battle() == settlement and settlement.battle_result == "" and round_guard > 0:
round_guard -= 1
st.rounds += 1
var start_round: int = b1.round_index
var start_round: int = settlement.round_index
var action_guard := 400
while runner.current_battle() == b1 and b1.battle_result == "" and b1.a3.phase == "Active" \
and b1.round_index == start_round and action_guard > 0:
while runner.current_battle() == settlement and settlement.battle_result == "" and settlement.action_economy.phase == "Active" \
and settlement.round_index == start_round and action_guard > 0:
action_guard -= 1
var plan := AutoPlayer.choose_dig(b1.a2)
var plan := AutoPlayer.choose_dig(settlement.minesweeper)
if plan.is_empty():
break
if plan.hero_cell != b1.a2.hero_pos():
var out_m: Array = b1.a2.settle_batch([{"action": "move", "cell": plan.hero_cell}])
if plan.hero_cell != settlement.minesweeper.hero_pos():
var out_m: Array = settlement.minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}])
if logger != null:
logger.log_action("move", plan.hero_cell, _accepted(out_m), {"round": start_round})
if b1.a3.phase != "Active" or b1.round_index != start_round:
if settlement.action_economy.phase != "Active" or settlement.round_index != start_round:
break
var out_d: Array = b1.a2.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
var out_d: Array = settlement.minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
if logger != null:
logger.log_action("dig", plan.dig_cell, _accepted(out_d), {"round": start_round})
if runner.current_battle() == b1 and b1.battle_result == "" and b1.a3.phase == "Active" and b1.round_index == start_round:
var out_s: Array = b1.a2.settle_batch([{"action": "stop"}])
if runner.current_battle() == settlement and settlement.battle_result == "" and settlement.action_economy.phase == "Active" and settlement.round_index == start_round:
var out_s: Array = settlement.minesweeper.settle_batch([{"action": "stop"}])
if logger != null:
logger.log_action("stop", Vector2i(-1, -1), _accepted(out_s), {"round": start_round})
# 完美策略档战斗驱动(2026-09-14):单步决策循环——move/dig/flag 经 A2 settle_batch 提交,
# refresh 经 b1.player_request_refresh(B1 核心编排:A3 扣 RAP + 退役 + 当场换新板),
# 完美策略档战斗驱动(2026-09-14):单步决策循环——move/dig/flag 经 排雷玩法 settle_batch 提交,
# refresh 经 settlement.player_request_refresh(战斗结算 核心编排:行动经济 扣 RAP + 退役 + 当场换新板),
# stop 出内环走统一停手提交。连续拒绝 ≥3 = 编排违约防御(fail-fast 语义,停手防死循环)。
static func _autoplay_battle_perfect(runner, b1, st: Dictionary, logger) -> void:
static func _autoplay_battle_perfect(runner, settlement, st: Dictionary, logger) -> void:
var round_guard := 60
while st.result == "" and runner.current_battle() == b1 and b1.battle_result == "" and round_guard > 0:
while st.result == "" and runner.current_battle() == settlement and settlement.battle_result == "" and round_guard > 0:
round_guard -= 1
st.rounds += 1
var start_round: int = b1.round_index
var start_round: int = settlement.round_index
var action_guard := 400
var rejects := 0
while runner.current_battle() == b1 and b1.battle_result == "" and b1.a3.phase == "Active" \
and b1.round_index == start_round and action_guard > 0 and rejects < 3:
while runner.current_battle() == settlement and settlement.battle_result == "" and settlement.action_economy.phase == "Active" \
and settlement.round_index == start_round and action_guard > 0 and rejects < 3:
action_guard -= 1
var act := AutoPlayer.perfect_action(b1.a2, b1)
var act := AutoPlayer.perfect_action(settlement.minesweeper, settlement)
var kind := String(act.get("kind", "stop"))
if kind == "stop":
break
var ok := false
if kind == "move":
ok = _submit_intent(b1, "move", act.get("cell"), logger, start_round)
ok = _submit_intent(settlement, "move", act.get("cell"), logger, start_round)
elif kind == "dig":
ok = _submit_intent(b1, "dig", act.get("cell"), logger, start_round)
ok = _submit_intent(settlement, "dig", act.get("cell"), logger, start_round)
elif kind == "flag":
ok = _submit_intent(b1, "flag_place", act.get("cell"), logger, start_round)
ok = _submit_intent(settlement, "flag_place", act.get("cell"), logger, start_round)
elif kind == "refresh":
ok = bool(b1.player_request_refresh())
ok = bool(settlement.player_request_refresh())
if logger != null:
logger.log_action("refresh", Vector2i(-1, -1), ok, {"round": start_round})
if ok:
@@ -182,14 +182,14 @@ static func _autoplay_battle_perfect(runner, b1, st: Dictionary, logger) -> void
rejects = 0
else:
rejects += 1
if runner.current_battle() == b1 and b1.battle_result == "" and b1.a3.phase == "Active" and b1.round_index == start_round:
var out_s: Array = b1.a2.settle_batch([{"action": "stop"}])
if runner.current_battle() == settlement and settlement.battle_result == "" and settlement.action_economy.phase == "Active" and settlement.round_index == start_round:
var out_s: Array = settlement.minesweeper.settle_batch([{"action": "stop"}])
if logger != null:
logger.log_action("stop", Vector2i(-1, -1), _accepted(out_s), {"round": start_round})
static func _submit_intent(b1, action: String, cell, logger, round_index: int) -> bool:
var out: Array = b1.a2.settle_batch([{"action": action, "cell": cell}])
static func _submit_intent(settlement, action: String, cell, logger, round_index: int) -> bool:
var out: Array = settlement.minesweeper.settle_batch([{"action": action, "cell": cell}])
if logger != null:
logger.log_action(action, cell, _accepted(out), {"round": round_index})
return _accepted(out)
@@ -1,6 +1,6 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Date: 2026-09-11
# 关卡期望扫描(C3 调参期关卡侧批量工具的原型——对应 A1 F4 / 战斗侧 monte_carlo.gd):
# 关卡期望扫描(难度曲线与Boss 调参期关卡侧批量工具的原型——对应 棋盘生成 F4 / 战斗侧 monte_carlo.gd):
# 扫描 atk% × hp% 网格(模拟天赋树数值提升档位),输出每档"通关率 / 整关承伤期望 / 败北节点分布 /
# 通关剩余 HP 期望 / 进 Boss HP 期望 / 平均轮数"——把"多少数值能通关、通关要掉多少血"变成可测量数据。
# 运行:Godot_console.exe --headless --path . --script res://smoke/level_monte_carlo.gd -- [N] [seed0]
@@ -53,8 +53,8 @@ func _initialize() -> void:
var hp_boss: Array = []
var rounds_all: Array = []
var actives_per_battle: Array = []
var normal_dmg_pct: Array = [] # 每普通战斗承伤占生效 max_hp 比(C3 F4 口径——≤20% 带)
var boss_dmg_pct: Array = [] # Boss 战承伤占生效 max_hp 比(C3 F4 口径——≤50% 带)
var normal_dmg_pct: Array = [] # 每普通战斗承伤占生效 max_hp 比(难度曲线与Boss F4 口径——≤20% 带)
var boss_dmg_pct: Array = [] # Boss 战承伤占生效 max_hp 比(难度曲线与Boss F4 口径——≤50% 带)
var wipes := 0 # 普通战斗 round-1 即胜数(首轮全灭跳战——拍板④观测面)
var normal_battles := 0
var death_nodes := {}
@@ -52,44 +52,44 @@ func _run_encounter(db, L: int) -> void:
func _run_battle(db, seed_val: int, L: int) -> Dictionary:
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(seed_val)
var b1 = load("res://core/battle_orchestrator.gd").new(db, rng)
var settlement = load("res://core/battle_orchestrator.gd").new(db, rng)
var out := {"rounds": 0, "result": "", "hero_dmg": 0, "safe_opened": 0, "board_swaps": 0, "buffs": 0}
b1.battle_finished.connect(func(payload): out.result = payload.get("result", ""))
b1.board_changed.connect(func(): out.board_swaps += 1)
b1.step_row.connect(func(row):
settlement.battle_finished.connect(func(payload): out.result = payload.get("result", ""))
settlement.board_changed.connect(func(): out.board_swaps += 1)
settlement.step_row.connect(func(row):
if row.type == "damage" and row.source_class == "enemy_action":
out.hero_dmg += int(row.value)
elif row.type == "effect" and row.source_class == "safe_cell" and (row.effect_id == "attack_up" or row.effect_id == "defense_up"):
out.buffs += 1)
b1.a2.cell_revealed.connect(func(evt):
settlement.minesweeper.cell_revealed.connect(func(evt):
if evt.kind == "safe" and evt.channel != "initial":
out.safe_opened += 1)
b1.window_opened.connect(func(items):
var s: Dictionary = b1.presentation_snapshot() # E1 收口:经呈现快照面读英雄/敌况
settlement.window_opened.connect(func(items):
var s: Dictionary = settlement.presentation_snapshot() # E1 收口:经呈现快照面读英雄/敌况
var selection: Array = AutoPlayer.window_selection(s, items,
{"heal_hp_div": 2, "round_gate": 0, "knife_compare": "sum"}) # O4:原 monte_carlo 口径
b1.submit_window_selection(selection))
if not b1.begin_battle({"level_index": L, "node_depth": 0, "hero_hp": int(db.hero.max_hp)}):
settlement.submit_window_selection(selection))
if not settlement.begin_battle({"level_index": L, "node_depth": 0, "hero_hp": int(db.hero.max_hp)}):
return out
var guard_rounds := 40
while out.result == "" and guard_rounds > 0:
guard_rounds -= 1
out.rounds += 1
var start_round: int = b1.round_index
var a2 = b1.a2
var start_round: int = settlement.round_index
var minesweeper = settlement.minesweeper
var guard := 400
while b1.a3.phase == "Active" and b1.round_index == start_round and guard > 0 and out.result == "":
while settlement.action_economy.phase == "Active" and settlement.round_index == start_round and guard > 0 and out.result == "":
guard -= 1
var plan := AutoPlayer.choose_dig(a2)
var plan := AutoPlayer.choose_dig(minesweeper)
if plan.is_empty():
break
if plan.hero_cell != a2.hero_pos():
a2.settle_batch([{"action": "move", "cell": plan.hero_cell}])
if b1.a3.phase != "Active" or b1.round_index != start_round:
if plan.hero_cell != minesweeper.hero_pos():
minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}])
if settlement.action_economy.phase != "Active" or settlement.round_index != start_round:
break
a2.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
if b1.a3.phase == "Active" and b1.round_index == start_round and out.result == "":
a2.settle_batch([{"action": "stop"}])
minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
if settlement.action_economy.phase == "Active" and settlement.round_index == start_round and out.result == "":
minesweeper.settle_batch([{"action": "stop"}])
if out.result == "":
out.result = "timeout"
return out
@@ -1,7 +1,7 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Date: 2026-09-11
# 奖励期望测量(供给控制器批口径):逐深度实测 |资格格|(safe ∖ R0)分布、每板注入件数(j=0 → 恒 1)、
# 注入种类分布(60/30/10 拍板值)、格子池零可选类验证——走真实 B4.roll_board 管线(含注入判定取号序)。
# 注入种类分布(60/30/10 拍板值)、格子池零可选类验证——走真实 奖励道具经济.roll_board 管线(含注入判定取号序)。
# 运行:Godot_console.exe --headless --path . --script res://smoke/reward_expectation.gd
extends SceneTree
@@ -14,8 +14,8 @@ func _initialize() -> void:
print("表加载失败")
quit(1)
return
var a1 = load("res://core/board_generator.gd").new()
var b4_factory = load("res://core/reward_economy.gd")
var board_generator = load("res://core/board_generator.gd").new()
var reward_economy_factory = load("res://core/reward_economy.gd")
var rng_factory = load("res://core/rng_service.gd")
var active_ids := {"knife_t1": true, "knife_adv": true, "heal_pack": true}
print("=== 奖励期望测量(供给控制器批 · N=%d 板/深度 · 每板全新战斗态 j=0)===" % N)
@@ -28,7 +28,7 @@ func _initialize() -> void:
for i in range(N):
var rng = rng_factory.new()
rng.set_master_seed(90000 + depth * 1000 + i)
var board = a1.generate(1, depth, rng.open_stream("board_gen"), db)
var board = board_generator.generate(1, depth, rng.open_stream("board_gen"), db)
if board == null:
continue
var r0 := {}
@@ -39,15 +39,15 @@ func _initialize() -> void:
if not r0.has(c):
eligible.append(c)
elig.append(eligible.size())
var b4 = b4_factory.new(db, rng)
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})
if not b4.roll_board(list):
if not reward_economy.roll_board(list):
continue
var n_active := 0
for c in eligible:
var snap = b4.query_identity(c)
var snap = reward_economy.query_identity(c)
if snap != null and active_ids.has(String(snap.reward_id)):
n_active += 1
species[String(snap.reward_id)] += 1.0
File diff suppressed because it is too large. Load diff
@@ -76,12 +76,12 @@ func _ready() -> void:
_done = true
print("[click_probe] 关卡结束:%s" % main._end_overlay.verdict_text().replace("\n", " "))
break
if main.b1 == null or main.runner == null:
if main.settlement == null or main.runner == null:
continue
var diag: Dictionary = main.b1.diagnostic_snapshot() # 诊断观测面(私有字段零直读)
var diag: Dictionary = main.settlement.diagnostic_snapshot() # 诊断观测面(私有字段零直读)
var st := {
"node": main.runner.node_index, "round": main.b1.round_index,
"phase": main.b1.a3.phase, "ap": main.b1.a3.ap,
"node": main.runner.node_index, "round": main.settlement.round_index,
"phase": main.settlement.action_economy.phase, "ap": main.settlement.action_economy.ap,
"win": main.inventory_panel.is_window_open(), "settling": diag.settling,
"await": diag.awaiting_window,
}
@@ -94,7 +94,7 @@ func _ready() -> void:
_stall_frames = 0
_ap_spent_this_round = 0
_last_state = st
if main.b1.battle_result == "" and main.b1.a3.phase == "Active":
if main.settlement.battle_result == "" and main.settlement.action_economy.phase == "Active":
_run_modal_probes_once()
_play_one_action()
print("[click_probe] 循环退出(done=%s budget=%s)选用生效=%s 选用失效=%s 问题=%d" % [
@@ -156,17 +156,17 @@ func _run_modal_probes_once() -> void:
# ④ 弃图刷新确认链(pattern 7 异位:确认 chips 与常规确认不同位 + 显式代价文案)
if not _tested_refresh_chain and main.ap_bar.is_refresh_ready():
_tested_refresh_chain = true
var rap_before: int = int(main.b1.a3.rap)
var board_before = main.b1.a2.current_board()
var rap_before: int = int(main.settlement.action_economy.rap)
var board_before = main.settlement.minesweeper.current_board()
main.ap_bar.press_refresh_for_probe()
if not main.ap_bar.confirm_visible():
_problems.append("弃图刷新确认 chips 未出现(异位确认缺失)")
main.ap_bar.press_confirm_for_probe()
if int(main.b1.a3.rap) != rap_before - 8:
_problems.append("弃图刷新未扣 RAP8(%d→%d)" % [rap_before, int(main.b1.a3.rap)])
if main.b1.a3.phase != "Active":
_problems.append("轮内刷新后相位丢失(%s)" % main.b1.a3.phase)
if main.b1.a2.current_board() == board_before:
if int(main.settlement.action_economy.rap) != rap_before - 8:
_problems.append("弃图刷新未扣 RAP8(%d→%d)" % [rap_before, int(main.settlement.action_economy.rap)])
if main.settlement.action_economy.phase != "Active":
_problems.append("轮内刷新后相位丢失(%s)" % main.settlement.action_economy.phase)
if main.settlement.minesweeper.current_board() == board_before:
_problems.append("轮内刷新未当场换图")
if main._refresh_used_round != 1:
_problems.append("刷新轮内计数错误(%d)" % main._refresh_used_round)
@@ -187,13 +187,13 @@ func _assert_window_state_once() -> void:
func _play_one_action() -> void:
var b1 = main.b1
if b1 == null:
return # 结算竞态防抖:battle_finished 与本轮动作调度同帧到达时 b1 可能已被置空(基线既有噪声——加固)
var a2 = b1.a2
var settlement = main.settlement
if settlement == null:
return # 结算竞态防抖:battle_finished 与本轮动作调度同帧到达时 settlement 可能已被置空(基线既有噪声——加固)
var minesweeper = settlement.minesweeper
# 行为①:随机插旗(右键——未开相邻格),约 20% 轮次
if _ap_spent_this_round == 0 and _rand.randf() < 0.2:
var target := _random_unknown_adjacent(a2)
var target := _random_unknown_adjacent(minesweeper)
if target.x >= 0:
main.board_view.click_cell(target, MOUSE_BUTTON_RIGHT)
_flags_placed.append(target)
@@ -202,33 +202,33 @@ func _play_one_action() -> void:
# 行为②:随机拔旗(右键已插旗格),约 10% 轮次
if _rand.randf() < 0.1 and not _flags_placed.is_empty():
var fc: Vector2i = _flags_placed.pop_back()
if a2.has_flag(fc) and not a2.is_revealed(fc):
if minesweeper.has_flag(fc) and not minesweeper.is_revealed(fc):
main.board_view.click_cell(fc, MOUSE_BUTTON_RIGHT)
_ap_spent_this_round += 1
return
# 行为③:随机无效点击(非相邻格/已开格),约 8% 轮次
if _rand.randf() < 0.08:
main.board_view.click_cell(Vector2i(_rand.randi_range(0, a2.current_board().width - 1), _rand.randi_range(0, a2.current_board().height - 1)), MOUSE_BUTTON_LEFT)
main.board_view.click_cell(Vector2i(_rand.randi_range(0, minesweeper.current_board().width - 1), _rand.randi_range(0, minesweeper.current_board().height - 1)), MOUSE_BUTTON_LEFT)
return
# 挖掘主策略(最近可挖格)
var plan = AutoPlayer.choose_dig(a2) # O4 收口:策略单一实现
var plan = AutoPlayer.choose_dig(minesweeper) # O4 收口:策略单一实现
if not plan.is_empty():
if plan.hero_cell != a2.hero_pos():
if plan.hero_cell != minesweeper.hero_pos():
main.board_view.click_cell(plan.hero_cell, MOUSE_BUTTON_LEFT)
main.board_view.click_cell(plan.dig_cell, MOUSE_BUTTON_LEFT)
_ap_spent_this_round += 1
# 行为④:提前停手(已花 ≥3 AP 时 15%)——停手经 AP 条按钮真实信号路径
if _ap_spent_this_round >= 3 and b1.a3.phase == "Active" and _rand.randf() < 0.15:
if _ap_spent_this_round >= 3 and settlement.action_economy.phase == "Active" and _rand.randf() < 0.15:
_click_stop()
elif b1.a3.phase == "Active":
elif settlement.action_economy.phase == "Active":
_click_stop()
func _answer_window() -> void:
# 行为⑥:选用首件可用道具 / 跳过(选择语义 = 窗口条目位置——与 UI 契约一致,缺陷④回归载体)
if _rand.randf() < 0.5 and main.b1 != null:
var b1 = main.b1
var s: Dictionary = b1.presentation_snapshot() # E1 收口:经呈现快照面读库存/英雄
if _rand.randf() < 0.5 and main.settlement != null:
var settlement = main.settlement
var s: Dictionary = settlement.presentation_snapshot() # E1 收口:经呈现快照面读库存/英雄
var pos := -1
for inst in s.inventory:
if inst.status != "InStock":
@@ -237,11 +237,11 @@ func _answer_window() -> void:
var usable: bool = int(s.hero.hp) < int(s.hero.max_hp) or inst.category == "damage_item"
if usable:
var inv_index: int = int(inst.index)
b1.submit_window_selection([pos])
settlement.submit_window_selection([pos])
main.inventory_panel.close_window()
# 缺陷④回归断言:被选件必须真实消耗(Used),不得误耗邻件/留存
var after: Array = b1.presentation_snapshot().inventory
if not b1.is_battle_live():
var after: Array = settlement.presentation_snapshot().inventory
if not settlement.is_battle_live():
# 终局结算窗口:选用后战斗立即结束、level_clear 合法清库——Used 断言不适用
_used_ok += 1
elif inv_index < after.size() and String(after[inv_index].status) == "Used":
@@ -258,20 +258,20 @@ func _answer_window() -> void:
func _skip_window() -> void:
if main.b1 != null:
main.b1.submit_window_selection([])
if main.settlement != null:
main.settlement.submit_window_selection([])
main.inventory_panel.close_window()
func _random_unknown_adjacent(a2) -> Vector2i:
var hero: Vector2i = a2.hero_pos()
func _random_unknown_adjacent(minesweeper) -> Vector2i:
var hero: Vector2i = minesweeper.hero_pos()
var cands: Array = []
for dy in range(-1, 2):
for dx in range(-1, 2):
if dx == 0 and dy == 0:
continue
var c: Vector2i = hero + Vector2i(dx, dy)
if a2.current_board().in_bounds(c) and not a2.is_revealed(c) and not a2.has_flag(c):
if minesweeper.current_board().in_bounds(c) and not minesweeper.is_revealed(c) and not minesweeper.has_flag(c):
cands.append(c)
if cands.is_empty():
return Vector2i(-1, -1)
@@ -279,7 +279,7 @@ func _random_unknown_adjacent(a2) -> Vector2i:
func _click_stop() -> void:
# 停手走真实按钮信号路径(AP 条 → 组合根 settle_batch——A2 D1×6① 回归载体)
# 停手走真实按钮信号路径(AP 条 → 组合根 settle_batch——排雷玩法 D1×6① 回归载体)
main.ap_bar._btn_stop.pressed.emit()
@@ -291,22 +291,22 @@ func _report_stall(st: Dictionary) -> void:
why = "窗口悬置未被消费(面板窗口态=%s)" % str(main.inventory_panel.is_window_open())
elif st.phase == "Active" and st.ap == 0:
why = "Active 相位 AP=0(应在归零瞬间翻 Waiting)"
elif st.phase == "Idle" and main.b1.battle_result == "":
elif st.phase == "Idle" and main.settlement.battle_result == "":
why = "Idle 相位但战斗未结束(begin 失败?)"
else:
why = "状态无推进(battle_result='%s')" % str(main.b1.battle_result)
why = "状态无推进(battle_result='%s')" % str(main.settlement.battle_result)
print("[click_probe] 🔴 卡死复现(PROBE_SEED=%s):%s" % [OS.get_environment("PROBE_SEED"), why])
_dump_state()
get_tree().quit(2)
func _dump_state() -> void:
var diag: Dictionary = main.b1.diagnostic_snapshot() # 诊断观测面(私有字段零直读)
var diag: Dictionary = main.settlement.diagnostic_snapshot() # 诊断观测面(私有字段零直读)
print(" node_index=%s round=%s phase=%s ap=%s rap=%s" % [
str(main.runner.node_index), str(main.b1.round_index),
str(main.b1.a3.phase), str(main.b1.a3.ap), str(main.b1.a3.rap)])
str(main.runner.node_index), str(main.settlement.round_index),
str(main.settlement.action_economy.phase), str(main.settlement.action_economy.ap), str(main.settlement.action_economy.rap)])
print(" battle_result='%s' settling=%s awaiting_window=%s window_visible=%s window_open=%s" % [
str(diag.battle_result), str(diag.settling), str(diag.awaiting_window),
str(main.inventory_panel._window_mode), str(main.inventory_panel.is_window_open())])
print(" board_installed=%s facts_path='%s' refresh_used=%s flags=%s" % [
str(diag.a2_board_installed), str(diag.facts_path), str(diag.refresh_used_waiting), str(_flags_placed)])
str(diag.minesweeper_board_installed), str(diag.facts_path), str(diag.refresh_used_round), str(_flags_placed)])
@@ -25,7 +25,7 @@ func _ready() -> void:
problems.append("节点进度条未构建(%d 子节点)" % main.strip_view.get_child_count())
if main.info_panel.hp_text().begins_with("HP —") or main.info_panel.hp_text().contains("—"):
problems.append("C 区血量未显示(%s)" % main.info_panel.hp_text())
if main.b1 == null or main.b1.battle_result != "":
if main.settlement == null or main.settlement.battle_result != "":
problems.append("首战未处于进行中")
if not main.log_view.parsed_text().contains("节点 1/"):
problems.append("节点头日志缺失")
@@ -46,7 +46,7 @@ func _ready() -> void:
problems.append("B 区明细带挖掘期应收起 24px(mode=%s h=%s)" % [main.log_view.mode(), str(main.log_view.custom_minimum_size.y)])
if main.inventory_panel._title.text != "道包 · 关卡域":
problems.append("E 区库存常读标题错误(%s)" % main.inventory_panel._title.text)
# ---- 敌人卡投影(顺位/意图徽标常驻——B2×4)----
# ---- 敌人卡投影(顺位/意图徽标常驻——敌人系统×4)----
var card = main.stage.enemy_box.get_child(0)
if card != null and card._name_label.text == "":
problems.append("敌人卡名称为空")
@@ -75,7 +75,29 @@ func _ready() -> void:
slots_row.set_effects([])
if not slots_row._slots.is_empty():
problems.append("BUG-0001 回归:空态未清槽(%d)" % slots_row._slots.size())
# ---- L2 雾图变体段(A4:雾首遇一次性提示 / 雾格呈现 / 去饱和接口可用性)----
# ---- 图标键回归(HUD框架 图标键契约:状态表六行全映射、奖励 category 五值全映射;
# 查不到的键 fail-fast + ? 占位——此处钉死映射不残缺,未知键断言由 _icon_cfg/_badge_glyph 承担)----
if not StatusSlotRow.EFFECT_ICON.has("enrage") or not StatusSlotRow.EFFECT_ICON.has("off_guard"):
problems.append("状态图标表缺暴走/失衡两键(states 表六行须全映射)")
for cat in ["instant_aoe", "instant_heal", "damage_item", "heal_pack", "buff"]:
if not BoardView.REWARD_BADGE.has(cat):
problems.append("奖励图标表缺键(%s)" % cat)
var probe_kb: Array = [
{"effect_id": "enrage", "display_name": "暴走", "layers": 1, "remaining_settlements": 1, "source_class": "flag_miss", "round_index": 1, "effective_value": 25},
{"effect_id": "off_guard", "display_name": "失衡", "layers": 1, "remaining_settlements": 1, "source_class": "flag_miss", "round_index": 1, "effective_value": 25},
]
slots_row.set_effects(probe_kb)
if slots_row._slots.size() != 2:
problems.append("图标键回归:enrage/off_guard 未建槽(%d)" % slots_row._slots.size())
else:
var ic_enrage: Label = slots_row._slots[0].get_child(0).get_child(0)
var ic_off: Label = slots_row._slots[1].get_child(0).get_child(0)
if ic_enrage.text == "?" or ic_enrage.text == "":
problems.append("暴走图标键未映射(icon=%s)" % ic_enrage.text)
if ic_off.text == "?" or ic_off.text == "":
problems.append("失衡图标键未映射(icon=%s)" % ic_off.text)
slots_row.set_effects([])
# ---- L2 雾图变体段(地形变异:雾首遇一次性提示 / 雾格呈现 / 去饱和接口可用性)----
# 2026-09-13 单场用户入口已删——探针直调 debug_session 原语(battle_created/ready 仍经组合根接线;
# _reset_run_scope 复现原 _start_single 的提示去重复位,保证 fog_first 断言前置态)
main._reset_run_scope()
@@ -85,7 +107,7 @@ func _ready() -> void:
if main.board_view.board() == null:
problems.append("L2 单场未启动(board=null)")
elif main.board_view.board().fog_cells().is_empty():
problems.append("L2 板无雾格(A4 变体缺失)")
problems.append("L2 板无雾格(地形变异 变体缺失)")
elif not bool(main._hints_shown.get("fog_first", false)):
problems.append("雾首遇一次性提示未触发(fog_first)")
else:
@@ -59,7 +59,7 @@ func _ready() -> void:
main._start_full_level() # 完整关卡路径(_start_single 按语义清零滑条 = 恒基线,不承载加成断言)
await get_tree().process_frame
await get_tree().process_frame
var hero: Dictionary = main.b1.presentation_snapshot().hero
var hero: Dictionary = main.settlement.presentation_snapshot().hero
if int(hero.get("atk", -1)) != 12 or int(hero.get("max_hp", -1)) != 75:
problems.append("战斗加成单次生效违例(atk=%s max_hp=%s,期望 12/75)" % [
str(hero.get("atk", "?")), str(hero.get("max_hp", "?"))])