# tests/unit/battle_settlement/seal_polarity_test.gd — 封印与极性单元测试宿主 # 断言面 = 八套(批 B 断言迁移自 smoke_all.gd,G5 施工组;八套按源编排序分八笔原子提交迁入): # 末点 AP 封印轮末结算(playtest 缺陷②回归——action_economy 末点花费与轮终结) # 封印不可逆四情况(禁撤封印 + 挖封印格兑付改判——minesweeper_rules 拒绝面) # 清图奖励双份兑付链(真实流完美封印——orchestrator grant_clear 身份兑付) # 清图奖励弱形式分支(零兑付 / 高光守卫 / 第二份拒——直调编排私有面构造) # 轮内刷新 provenance 守卫(刷新图清图不发清图奖励) # 极性路由 + 封印溢价 125%(挖安全格正向 / 挖危险格负向 / 标中标空兑付极性) # 负向字典逐型直验(效果行 / heal 直伤 / blast 警觉 / action 行动点排队) # 格面资源资格面(揭示事件 resource 标记 + 封锁格身份查询) # 协议照单元侧惯例:extends SceneTree+Kit.check 主通道;--script 模式无 autoload, # ConfigDB 显式实例化构造注入(各套自建 RngService——源套掷点隔离形态,宿主不持统一流); # 表加载失败不中止(沿被迁断言面的源语义——诊断在此展开,表值走偏的红由具体断言落,承 G2/G3/G4 同款)。 # 运行:godot --headless --path game --script res://tests/unit/battle_settlement/seal_polarity_test.gd extends SceneTree const Kit := preload("res://tests/unit/test_kit.gd") const ConfigDBScript := preload("res://core/config_db.gd") const AutoPlayer := preload("res://sim/auto_player.gd") # 自动玩家策略单一实现(迁移期按现路径 preload——账本 §五夹具前提) var _db func _init() -> void: # 判据出处: design/gdd/systems/排雷循环/排雷玩法.md:231 (封印与极性宿主——装载组) _db = ConfigDBScript.new() # 表加载失败不中止(承 G2/G3/G4 同款:表损坏的红落在具体断言上—— # 诊断在此展开,后续断言按坏表实况逐条验红;collect_only=true 下成员照常赋值可评) var load_ok: bool = _db.load_tables("res://data/tables", true) if not load_ok: push_error("[seal_polarity_test] 表加载返回失败——诊断如下(红由具体断言落)") for d in _db.diagnostics(): if d.severity == "error" or d.severity == "fatal": push_error(" [%s] %s: %s" % [d.severity, d.category, d.message]) # 套执行序沿源编排序 _smoke_seal_ap_exhausted() _smoke_clear_reward_chain() _smoke_refresh_provenance_guard() _smoke_polarity_routing() _smoke_seal_irreversible_quadrants() _smoke_negative_dictionary() _smoke_resource_surface() _check_enemy_alert_production_row() _check_ap_drain_production_row() quit(Kit.finish("seal_polarity_test")) # ---- 末点 AP 封印轮末结算(playtest 缺陷②回归:末点 AP 花在封印必须终结轮次进入下一轮)---- func _smoke_seal_ap_exhausted() -> void: # 判据出处: design/gdd/systems/排雷循环/行动经济.md:260 (末点 AP 耗尽即收轮组) print("--- 排雷玩法·封印动作耗尽末点 AP 回归 ---") var rng = load("res://core/rng_service.gd").new() rng.set_master_seed(20260911) var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng) var rounds: Array = [] settlement.round_started.connect(func(ri): rounds.append(ri)) Kit.check("回归战斗启动(Boss 行——D0 20HP 级会被盲挖粉尘秒杀,无法承载封印回归场景)", settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"})) var minesweeper = settlement.minesweeper # 恰花 (AP-1) 点挖掘,留末点给封印(表驱动——AP 从表读不写死) var ap_total: int = int(_db.run.action_economy.ap_per_round) for i in range(ap_total - 1): if settlement.action_economy.phase != "Active" or settlement.action_economy.ap == 0: break var plan = AutoPlayer.choose_dig(minesweeper) if plan.is_empty(): break if plan.hero_cell != minesweeper.hero_pos(): minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}]) minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}]) Kit.check("%d 次挖掘后恰剩 1 AP(phase=%s ap=%d)" % [ap_total - 1, str(settlement.action_economy.phase), settlement.action_economy.ap], settlement.action_economy.phase == "Active" and settlement.action_economy.ap == 1) # 末点 AP 花在封印——轮次必须终结并结算进入第 2 轮 # 封印目标 = 挖掘策略的下一目标格(构造保证:未开、与小人邻接——先把小人移到其邻接已开格) var plan2 = AutoPlayer.choose_dig(minesweeper) Kit.check("仍存在可挖目标(封印目标来源)", not plan2.is_empty()) var out: Array = [] if not plan2.is_empty(): if plan2.hero_cell != minesweeper.hero_pos(): minesweeper.settle_batch([{"action": "move", "cell": plan2.hero_cell}]) out = minesweeper.settle_batch([{"action": "seal_place", "cell": plan2.dig_cell}]) Kit.check("末点 AP 封印被受理", not out.is_empty() and bool(out[0].accepted)) Kit.check("封印动作耗尽 AP 触发轮末结算并进入第 2 轮(rounds=%s)" % str(rounds), rounds.size() >= 2) # ==================== 清图奖励双份兑付链 / 轮内刷新 provenance 守卫 / 窗口锚行 ==================== # 清图 = 全安全格开完。清图测试用"真雷封印(零误标→完美封印)+ AutoPlayer 逐轮清图"真实流驱动; # DifficultyTable 注入 hp_mult=10 把 Boss 血量抬到 1500,保证清图批次发生在战斗结束之前(测试台账,跑完即还原)。 const CLEAR_SMOKE_DEPTH := 4 const CLEAR_SMOKE_HERO_HP := 100000 var _regular_clear_reward_rows := 0 # 常规(非刷新图)清图的 clear_reward 行数——刷新守卫对照基线 # 板真相直读找封印目标:未开 + 指定雷/安全属性 + 存在可达已开邻格(测试允许读板真相——排雷玩法 冒烟同口径) func _find_seal_target(minesweeper, want_mine: bool) -> Dictionary: var revealed: Array = minesweeper.revealed_cells() var revealed_set := {} for c in revealed: revealed_set[c] = true for y in minesweeper.current_board().height: for x in minesweeper.current_board().width: var c := Vector2i(x, y) if minesweeper.is_revealed(c) or minesweeper.has_seal(c): continue if minesweeper.current_board().is_mine(c) != want_mine: continue for n in minesweeper.current_board().neighbors8(c): if revealed_set.has(n) and (minesweeper.compute_move_path(n).size() > 0 or n == minesweeper.hero_pos()): return {"seal_cell": c, "hero_cell": n} return {} # ---- 封印不可逆四情况冒烟(禁撤封印 + 挖封印格兑付改判)---- # 四情况:① 插危险格不动 = 标中奖励照常;② 插危险格再挖开 = 只受挖雷一层惩罚(mine_hit 照常)标中不发; # ③ 插安全格不动 = 标空惩罚照常;④ 插安全格再挖开 = 零惩罚零奖励(正向兑付作废、标空不判)。 # 拒绝面:seal_pull 出目录(out_of_catalog);重复封印仍 seal_present 拒绝。 # 场景 A(④ + 拒绝面):标安全格 → seal_pull 拒绝 → 重复封印拒绝 → 直接挖开 → 收轮。 # 场景 B(②):标危险格 → 直接挖开 → 收轮。 # 场景 C(①③):标安全格 + 标危险格 → 不挖收轮 → 标空/标中判定照常。 func _auto_play_round(settlement, out: Dictionary) -> void: var minesweeper = settlement.minesweeper var action_economy = settlement.action_economy var start_round: int = settlement.round_index # 确定性自动玩家:挖最近的已探明邻接未开格;无可挖则停手(结算窗口由信号回调提交) var guard := 400 while action_economy.phase == "Active" and settlement.round_index == start_round and guard > 0 and out.result == "": guard -= 1 var plan := AutoPlayer.choose_dig(minesweeper) if plan.is_empty(): break if plan.hero_cell != minesweeper.hero_pos(): minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}]) if action_economy.phase != "Active" or settlement.round_index != start_round: break minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}]) if action_economy.phase == "Active" and settlement.round_index == start_round and out.result == "": minesweeper.settle_batch([{"action": "stop"}]) # ---- 行动经济·轮内刷新冒烟(方案 A:Active 相位当场换图继续挖)---- func _run_battle(L: int, D: int, axes: Dictionary = {}) -> Dictionary: # 每遍全新 RngService(同 master 种子)——回放断言的隔离前提(地图战斗资源 AC"两套全新 RngService+地图战斗资源"同款) var rng = load("res://core/rng_service.gd").new() rng.set_master_seed(42) var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng) var out := {"stream": [], "events": [], "rounds": 0, "result": "", "terminated": false} settlement.step_row.connect(func(row): out.stream.append(row)) settlement.battle_finished.connect(func(payload): out.result = payload.get("result", "")) settlement.minesweeper.cell_revealed.connect(func(evt): out.events.append(evt)) settlement.minesweeper.seal_judged.connect(func(evt): out.events.append(evt)) settlement.minesweeper.round_end_board.connect(func(p): out.events.append({"ev": "round_end_board", "path_tag": p.path_tag})) settlement.action_economy.round_end_stopped.connect(func(p): out.events.append({"ev": "round_end_stopped", "path_tag": p.path_tag})) var enc := {"level_index": L, "node_depth": D, "hero_hp": _db.combat.hero.max_hp} enc.merge(axes, true) # 两根类型轴注入尾参(缺省零注入=回退面) if not settlement.begin_battle(enc): out.stream.append({"type": "verdict", "value": "begin_failed"}) return out var max_rounds := 40 while out.result == "" and out.rounds < max_rounds: out.rounds += 1 _auto_play_round(settlement, out) if out.result == "": out.result = "timeout" out.terminated = out.result == "victory" or out.result == "defeat" return out # 清图驱动(真实流):标一面真雷封印(完美前置)→ 逐轮 AutoPlayer 挖到轮末;board_seq 递增 = 清图换图。 # from_refresh=true 时先三轮停手攒 RAP、第 4 轮轮内刷新换图,再清刷新图。 func _run_full_clear(seed_val: int, from_refresh: bool, rows: Array) -> Dictionary: _db.run.difficulty.rows.append({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hp_mult": 10.0, "atk_mult": 1.0}) var rng = load("res://core/rng_service.gd").new() rng.set_master_seed(seed_val) var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng) settlement.step_row.connect(func(row): rows.append(row)) var res := {"cleared": false, "refreshed": false, "rounds": 0} if not settlement.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "node_type": "boss", "hero_hp": CLEAR_SMOKE_HERO_HP, "hero_max_hp": CLEAR_SMOKE_HERO_HP}): _db.run.difficulty.rows.clear() return res if from_refresh: for i in range(3): settlement.minesweeper.settle_batch([{"action": "stop"}]) res["refreshed"] = settlement.player_request_refresh() var drive := _drive_clear(settlement) res["cleared"] = drive.cleared res["rounds"] = drive.rounds _db.run.difficulty.rows.clear() # 测试台账还原(后续冒烟基线) return res func _drive_clear(settlement) -> Dictionary: var seal_done := false var rounds := 0 while rounds < 60: rounds += 1 var ms = settlement.minesweeper if not seal_done: var t := _find_seal_target(ms, true) if not t.is_empty(): if t.hero_cell != ms.hero_pos(): ms.settle_batch([{"action": "move", "cell": t.hero_cell}]) ms.settle_batch([{"action": "seal_place", "cell": t.seal_cell}]) seal_done = true # 一面真雷封印 = 完美封印前半(零误标),后半靠 AutoPlayer 清图 var seq_before: int = int(settlement.diagnostic_snapshot().board_seq) var out := {"result": "", "rounds": 0, "terminated": false, "stream": []} _auto_play_round(settlement, out) if int(settlement.diagnostic_snapshot().board_seq) > seq_before: return {"cleared": true, "rounds": rounds} # 清图换图 = board_seq 递增 if settlement.battle_result != "": return {"cleared": false, "rounds": rounds} return {"cleared": false, "rounds": rounds} func _rows_of_type(rows: Array, t: String) -> Array: var out: Array = [] for r in rows: if String(r.get("type", "")) == t: out.append(r) return out func _has_source_row(rows: Array, t: String, source: String, batch_key: String = "") -> bool: for r in rows: if String(r.get("type", "")) != t or String(r.get("source_class", "")) != source: continue if batch_key == "" or String(r.get("batch_key", "")) == batch_key: return true return false func _has_effect_id(rows: Array, effect_id: String) -> bool: for r in rows: if String(r.get("effect_id", "")) == effect_id: return true return false # 手搓战斗状态袋单点(7 处字面量收在一处;_autoplay_battle 直接消费的最小字段族。 # 大字段族同构袋仍走 _adv_st()——_drive_level/_autoplay_battle 全字段消费面) const POSITIVE_EFFECTS := ["attack_up", "ac_up", "weaken", "gp_gain"] const NEGATIVE_EFFECTS := ["enrage", "off_guard", "sapped", "alert", "gp_loss"] # 构造验收谓词(hunt 用——种子按行为面扫描:四触达 + 标空封印负向兑付行在案) func _polarity_probe_accepts(probe: Dictionary) -> bool: if not (bool(probe["seal_hit_done"]) and bool(probe["seal_miss_done"])): return false if int(probe["mine_events"]) < 1 or int(probe["safe_events"]) < 1: return false var miss_rows := _rows_at(probe["rows"], "seal_miss", probe["miss_cell"]) if miss_rows.is_empty(): return false for r in miss_rows: if String(r.get("type", "")) == "seal_miss": continue var eff := String(r.get("effect_id", "")) if eff != "" and not NEGATIVE_EFFECTS.has(eff): return false return true # 极性路由探针驱动:标 1 面真雷封印(标中)+ 1 面安全格封印(标空)→ AutoPlayer 挖到战斗终局(多轮样本)。 # 收集全部 cell_revealed / seal_judged 事件与结算行;返回格子集合与封印格坐标。 func _run_polarity_probe(seed_val: int) -> Dictionary: _db.run.difficulty.rows.append({"level_index": 1, "node_depth": 0, "hp_mult": 10.0, "atk_mult": 0.5}) # 抬敌血压低敌攻——多轮挖掘样本不被终局截断 var rng = load("res://core/rng_service.gd").new() rng.set_master_seed(seed_val) var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng) var probe := {"rows": [], "safe_cells": {}, "mine_cells": {}, "hit_cell": [], "miss_cell": [], "mine_events": 0, "safe_events": 0, "seal_hit_done": false, "seal_miss_done": false} settlement.step_row.connect(func(row): probe["rows"].append(row)) settlement.minesweeper.cell_revealed.connect(func(evt): if String(evt.get("channel", "")) == "initial": return if evt.kind == "mine": probe["mine_cells"][evt.cell] = true probe["mine_events"] = int(probe["mine_events"]) + 1 elif evt.kind == "safe": probe["safe_cells"][evt.cell] = true probe["safe_events"] = int(probe["safe_events"]) + 1) settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 300, "hero_max_hp": 300}) var ms = settlement.minesweeper # 封印 1:真雷(标中 → 正向);封印 2:安全格(标空 → 负向) var mine_t := _find_seal_target(ms, true) var safe_t := _find_seal_target(ms, false) if not mine_t.is_empty(): if mine_t.hero_cell != ms.hero_pos(): ms.settle_batch([{"action": "move", "cell": mine_t.hero_cell}]) ms.settle_batch([{"action": "seal_place", "cell": mine_t.seal_cell}]) probe["hit_cell"] = [mine_t.seal_cell.x, mine_t.seal_cell.y] probe["seal_hit_done"] = true if not safe_t.is_empty(): if safe_t.hero_cell != ms.hero_pos(): ms.settle_batch([{"action": "move", "cell": safe_t.hero_cell}]) ms.settle_batch([{"action": "seal_place", "cell": safe_t.seal_cell}]) probe["miss_cell"] = [safe_t.seal_cell.x, safe_t.seal_cell.y] probe["seal_miss_done"] = true # 挖到终局(封印挡链不影响 AutoPlayer——choose_dig 跳过封印格) var out := {"result": "", "rounds": 0, "terminated": false, "stream": []} var guard := 40 while settlement.battle_result == "" and guard > 0: guard -= 1 _auto_play_round(settlement, out) _db.run.difficulty.rows.clear() # 测试台账还原 return probe func _rows_at(rows: Array, source_class: String, cell) -> Array: var out: Array = [] for r in rows: if String(r.get("source_class", "")) == source_class and r.get("origin_cell", [9, 9]) == cell: out.append(r) return out # 负向字典逐型直验(类型化兑付行协议):找真实板上各型非封锁格,直调 grant_routed 负向路由(att 归因束形态), # 断言每型的兑付形态(enrage/off_guard/sapped 效果行、heal 直伤行、blast 警觉行、action 行动点排队)。 func _smoke_clear_reward_chain() -> void: # 判据出处: design/gdd/systems/战斗循环/地图战斗资源.md:235 (清图奖励链——清图槽对象身份组) # 判据出处: design/gdd/systems/战斗循环/地图战斗资源.md:235 (清图奖励链——清图槽对象身份组) print("--- 清图奖励·双份兑付链(完美封印 / 零兑付 / 第二份被拒 / 高光记实)---") # ① 真实流完美封印:标中一面真雷封印 + 零误标 + 清全图 → grant_clear 身份(clear_blast)双份真实兑付 var rows: Array = [] var run := _run_full_clear(20260915, false, rows) Kit.check("① 完美封印清图达成(%d 轮)" % int(run.rounds), bool(run.cleared)) var dmg := [] for r in rows: if String(r.get("type", "")) == "damage" and String(r.get("source_class", "")) == "clear_reward": dmg.append(r) var hl := _rows_of_type(rows, "highlight") _regular_clear_reward_rows = dmg.size() Kit.check("① 清图奖励双份真实兑付(clear_reward 伤害行 ×%d——clear_blast 身份两份各打一发)" % dmg.size(), dmg.size() == 2) if dmg.size() == 2: Kit.check("① 第二份按身份类型走真实兑付(首份『清图奖励』/ 第二份『完美第二份』)", String(dmg[0].display_name).contains("清图奖励") and String(dmg[1].display_name).contains("完美第二份")) Kit.check("④ 高光行 value = 实际有效份数 2(不记名义值;行数 %d)" % hl.size(), hl.size() == 1 and int(hl[0].value) == 2) if hl.size() == 1: Kit.check("高光标记 = perfect_sweep", String(hl[0].get("highlight", "")) == "perfect_sweep") # ②/③ 弱形式:正常战斗流不可达的零兑付分支(见函数头注) _smoke_clear_reward_zero_paths() # 弱形式登记:正常战斗流下"授段时刻无存活敌"不可达——敌全灭当轮判定段即判胜并终止战斗,而清图批在判定段之前, # 恒有存活敌;故本组直调编排私有面构造状态,只断言"路径存在 + 行为符合契约"(构造手法见各行注释)。 # 弱形式登记:正常战斗流下"授段时刻无存活敌"不可达——敌全灭当轮判定段即判胜并终止战斗,而清图批在判定段之前, # 恒有存活敌;故本组直调编排私有面构造状态,只断言"路径存在 + 行为符合契约"(构造手法见各行注释)。 func _smoke_clear_reward_zero_paths() -> void: # 判据出处: design/gdd/systems/战斗循环/地图战斗资源.md:240 (无存活敌群伤记「奖励未生效」组) # 判据出处: design/gdd/systems/战斗循环/地图战斗资源.md:240 (无存活敌群伤记「奖励未生效」组) # ②-a 首份零兑付判定(无存活敌)→ first_ok=false + 未生效行 var rng = load("res://core/rng_service.gd").new() rng.set_master_seed(20260915) var s = load("res://core/battle_orchestrator.gd").new(_db, rng) var rows: Array = [] s.step_row.connect(func(row): rows.append(row)) Kit.check("零兑付构造战斗启动", s.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80, "node_type": "boss"})) for u in s._enemy_system.all_enemies(): s._enemy_system.apply_damage(u, int(u.hp)) # 测试通道:构造"无存活敌"(弱形式——全灭而非 despawn) var snap = s._reward_economy.grant_clear() Kit.check("清图槽身份 = blast 型(clear_default 唯一活行 clear_blast——第二份同身份兑付)", snap != null and String(snap.cell_type) == "blast") rows.clear() var first_ok: bool = s.reward_grant()._grant_clear_identity(snap, 1, "(清图奖励)") Kit.check("② 即时类首份零兑付(无存活敌)→ first_ok=false", not first_ok) Kit.check("② 零兑付产出 reward_ineffective 行(batch_key=clear_blast)", _has_source_row(rows, "reward_ineffective", "clear_reward", "clear_blast")) # ②-b 高光守卫:perfect 谓词成立但首份零兑付 → 高光行不发(不记名义值) var rng_b = load("res://core/rng_service.gd").new() rng_b.set_master_seed(20260915) var s_b = load("res://core/battle_orchestrator.gd").new(_db, rng_b) var rows_b: Array = [] s_b.step_row.connect(func(row): rows_b.append(row)) Kit.check("高光守卫构造战斗启动", s_b.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80, "node_type": "boss"})) for u in s_b._enemy_system.all_enemies(): s_b._enemy_system.apply_damage(u, int(u.hp)) s_b._facts.perfect_hit_true = 1 # 测试通道:构造完美谓词成立(真值在正常流由封印判定累加) s_b._facts.perfect_hit_false = 0 rows_b.clear() s_b.reward_grant().grant_clear_reward(1, s_b._facts.board_from_midround, s_b.perfect_sweep_earned()) Kit.check("② grant_clear_reward 守卫路径重走首份 → 未生效行仍在案", _has_source_row(rows_b, "reward_ineffective", "clear_reward", "clear_blast")) Kit.check("② 首份零兑付 → 高光行不发(不记名义值 2)", _rows_of_type(rows_b, "highlight").is_empty()) # ③ 第二份被拒:唯一存活敌 HP ≤ 首份 aoe 量 → 首份击杀 → 第二份无存活敌(zero + 未生效行) var rng2 = load("res://core/rng_service.gd").new() rng2.set_master_seed(20260915) var s2 = load("res://core/battle_orchestrator.gd").new(_db, rng2) var rows2: Array = [] s2.step_row.connect(func(row): rows2.append(row)) Kit.check("第二份被拒构造战斗启动", s2.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80, "node_type": "boss"})) var boss = s2._enemy_system.all_enemies()[0] boss.hp = 1 # 测试通道:压低至首份 aoe 必杀(弱形式——正常流 Boss 血量不可控) s2._facts.perfect_hit_true = 1 s2._facts.perfect_hit_false = 0 rows2.clear() s2.reward_grant().grant_clear_reward(1, s2._facts.board_from_midround, s2.perfect_sweep_earned()) Kit.check("③ 首份 aoe 真实兑付(击杀唯一存活敌)", _has_source_row(rows2, "damage", "clear_reward")) Kit.check("③ 第二份被拒产出 reward_ineffective 行(零兑付)", _has_source_row(rows2, "reward_ineffective", "clear_reward", "clear_blast")) var hl2 := _rows_of_type(rows2, "highlight") Kit.check("④ 高光行 value = 实际有效份数 1(不记名义值 2)", hl2.size() == 1 and int(hl2[0].value) == 1) # 轮内刷新 provenance 守卫:刷新换来的图同轮清图不发清图奖励(_board_from_midround_refresh 守卫) # 轮内刷新 provenance 守卫:刷新换来的图同轮清图不发清图奖励(_board_from_midround_refresh 守卫) func _smoke_refresh_provenance_guard() -> void: # 判据出处: design/gdd/systems/排雷循环/排雷玩法.md:237 (换图重置:旧封印印记先判定后消失组) # 判据出处: design/gdd/systems/排雷循环/排雷玩法.md:237 (换图重置:旧封印印记先判定后消失组) print("--- 轮内刷新 provenance 守卫(刷新图清图不发清图奖励)---") var rows: Array = [] var run := _run_full_clear(20260916, true, rows) Kit.check("轮内刷新受理(三轮停手攒 RAP 后 Active 相位发起)", bool(run.refreshed)) Kit.check("刷新图清图达成(%d 轮)" % int(run.rounds), bool(run.cleared)) var cr := 0 var hl := 0 for r in rows: if String(r.get("source_class", "")) == "clear_reward": cr += 1 if String(r.get("type", "")) == "highlight": hl += 1 Kit.check("刷新图清图 → 无 clear_reward 行(守卫在位——实测 %d 行)" % cr, cr == 0) Kit.check("刷新图清图 → 无高光行(守卫在位——实测 %d 行)" % hl, hl == 0) Kit.check("对照基线:常规换图清图有 clear_reward 行(×%d——守卫非全局静默)" % _regular_clear_reward_rows, _regular_clear_reward_rows > 0) # ==================== 格面资源批新增覆盖(极性路由 / 负向字典 / 资源面 / 五段全序) ==================== # 极性路由断言(战斗结算 验收):挖安全格/封印命中只出正向、挖危险格/标错封印只出负向、兑付行指回来源格。 # 正向字典效果集 = {attack_up, ac_up, weaken} # + spark_aoe/heal_minor(flat)+ gp_gain(GP 袋,escrow 缓冲路由键非增益容器行); # 负向字典效果集 = {enrage, off_guard, sapped}(percent)+ 直伤(heal flat)/ alert 警觉(blast percent) # + gp_loss(破财,escrow 缓冲路由键)。 func _smoke_polarity_routing() -> void: # 判据出处: design/gdd/systems/战斗循环/战斗结算.md:366 (极性路由:正向只兑正向、负向只兑负向组) # 判据出处: design/gdd/systems/战斗循环/战斗结算.md:366 (极性路由:正向只兑正向、负向只兑负向组) print("--- 极性路由(挖安全格正向 / 挖危险格负向 / 标中正向 / 标空负向)---") # 战斗随机值面随全局序漂移——构造种子按行为面扫描(谓词见 _polarity_probe_accepts) var probe := {} var polarity_seed := -1 for s in [20260918, 20260919, 20260920, 20260921, 42, 4242, 7, 100, 20260922, 123, 2024, 31337, 555, 1111, 2222, 20260923, 999, 777]: probe = _run_polarity_probe(s) if _polarity_probe_accepts(probe): polarity_seed = s break # 触达三腿(标中标空入队/挖危险格/挖安全格)是 _polarity_probe_accepts 的合取项, # 可达即三腿同真——缺腿明细进标签承载诊断,不再各立一条(2026-10-28 断言精简批删并) var legs := "" if not (bool(probe["seal_hit_done"]) and bool(probe["seal_miss_done"])): legs += "标中标空未入队 " if int(probe["mine_events"]) < 1: legs += "挖危险格零 " if int(probe["safe_events"]) < 1: legs += "挖安全格零 " Kit.check("极性路由构造种子可达(seed=%d——并流后行为面重锚;缺腿:%s)" % [polarity_seed, legs], polarity_seed > 0) # 挖到的安全格:只出正向——其全部来源行必存在且 effect 行 ∈ 正向集 # (action 型排队行 effect_id=ap_delta 正负两极共用路由键——按行值符号辨极性:正向格恒 +power) var safe_all_pos := true for e in probe["safe_cells"].keys(): var cell_arr: Array = [e.x, e.y] var rows := _rows_at(probe["rows"], "safe_cell", cell_arr) if rows.is_empty(): safe_all_pos = false # 每个资源格必有 ≥1 兑付行(每格必有货 → 每格必有行) for r in rows: var eff := String(r.get("effect_id", "")) if eff == "ap_delta": if int(r.get("value", 0)) <= 0: safe_all_pos = false continue if eff != "" and not POSITIVE_EFFECTS.has(eff): safe_all_pos = false Kit.check("挖安全格 → 只出正向兑付(效果行 ∈ 正向字典 + 每格 ≥1 行——%d 格)" % probe["safe_cells"].size(), safe_all_pos) # 挖到的危险格:只出负向——来源行必存在且 effect 行 ∈ 负向集(alert = blast 负向;heal 负向 = damage 行; # action 型排队行同按行值符号辨极性:危险格恒 −power) var mine_all_neg := true for e in probe["mine_cells"].keys(): var cell_arr: Array = [e.x, e.y] var rows := _rows_at(probe["rows"], "mine_hit", cell_arr) if rows.is_empty(): mine_all_neg = false for r in rows: var eff := String(r.get("effect_id", "")) if eff == "ap_delta": if int(r.get("value", 0)) >= 0: mine_all_neg = false continue if eff != "" and not NEGATIVE_EFFECTS.has(eff): mine_all_neg = false Kit.check("挖危险格 → 只出负向兑付(效果行 ∈ 负向字典 + 每格 ≥1 行——%d 格)" % probe["mine_cells"].size(), mine_all_neg) # 封印命中:该危险格正向兑付行(seal_hit 来源)+ 效果 ∈ 正向字典 var hit_ok := false for r in probe["rows"]: if String(r.get("source_class", "")) == "seal_hit" and r.get("origin_cell", [0, 0]) == probe["hit_cell"]: hit_ok = true var eff := String(r.get("effect_id", "")) if eff == "ap_delta": if int(r.get("value", 0)) <= 0: hit_ok = false continue if eff != "" and not POSITIVE_EFFECTS.has(eff): hit_ok = false Kit.check("封印命中 → 该危险格资源的正向兑付行(origin 指回危险格,效果 ∈ 正向字典;action 型按行值符号辨极性)", hit_ok) # 条款3 条件二封印溢价断言(标中兑付行数值 = 同型正向 ×(1+δ)): # δ 读 settlement.seal_hit_premium(125 provisional);九型正向行值全固定(唯一区间行 clear_blast # 系清图槽不入格池)⇒ 逐型期望全确定。整数量级四舍五入域注记:ac_up/action 1 点量纲取整后不变 # =整数粒度地板——等号压线判读在当量聚合面(audit 工具/registry clause3_inequality),非断言面。 var prem_pct: int = int(_db.combat.settlement.core.seal_hit_premium) Kit.check("封印溢价配置在位(seal_hit_premium=125 provisional——形态 a 兑付溢价)", prem_pct == 125) var prem_rows := 0 var prem_ok := true for r in probe["rows"]: if String(r.get("source_class", "")) != "seal_hit" or String(r.get("highlight", "")) != "seal_hit": continue prem_rows += 1 var rtype := String(r.get("type", "")) var v := int(r.get("value", 0)) var eff := String(r.get("effect_id", "")) if rtype == "effect": if eff == "gp_gain": prem_ok = prem_ok and v == int(round(3 * prem_pct / 100.0)) # GP 袋 3→4 elif eff == "ap_delta": prem_ok = prem_ok and v == 1 # +1AP:1 点量纲(round(1.25)=1——整数粒度地板) else: var mag: int = int(_db.combat.status_effects.row_by_id(eff).get("magnitude", -999)) prem_ok = prem_ok and v == int(round(mag * prem_pct / 100.0)) # 增伤50→63/削弱65→81/护甲1→1 elif rtype == "heal": # 急救 2→3(clamp 封顶只降不升:满血走 reward_ineffective 不入本行——区间断言) prem_ok = prem_ok and v >= 1 and v <= int(round(2 * prem_pct / 100.0)) elif rtype == "damage": var at := String(r.get("attack_type", "")) if at == "": prem_ok = prem_ok and v == int(round(1 * prem_pct / 100.0)) # 爆裂粉尘 1→1(群伤行值=表值直付) else: # 穿刺/魔法 2→3(行值=落地伤=基准+克制点数——反推基准核对;damage_floor 兜底下成立) prem_ok = prem_ok and (v - int(r.get("effectiveness", 0))) == int(round(2 * prem_pct / 100.0)) Kit.check("标中兑付溢价(×%d 行逐型=同型正向 ×%d/100——等号压线注记:δ=1.25 时条件二 E[标中]=1.25·E_dig 恰等成立)" % [prem_rows, prem_pct], prem_rows >= 1 and prem_ok) # 标空不乘溢价:负向直伤行(反噬6/陷阱8/魔法反噬8 固定表值)零溢价污染——溢价值挂标中正向单点 var miss_flat_ok := true for r in _rows_at(probe["rows"], "seal_miss", probe["miss_cell"]): if String(r.get("type", "")) == "damage": miss_flat_ok = miss_flat_ok and int(r.get("value", 0)) in [6, 8] Kit.check("标空惩罚零溢价(负向直伤行=固定表值原值——溢价只挂标中正向)", miss_flat_ok) # 标空封印:归因行 + 该安全格负向兑付行(seal_miss 来源) var miss_rows := _rows_at(probe["rows"], "seal_miss", probe["miss_cell"]) var miss_neg_ok := false for r in miss_rows: if String(r.get("type", "")) == "seal_miss": miss_neg_ok = true else: var eff := String(r.get("effect_id", "")) if eff != "" and not NEGATIVE_EFFECTS.has(eff): miss_neg_ok = false Kit.check("标空封印 → 封印未中归因行 + 该安全格资源负向兑付(同格同批)", not miss_rows.is_empty() and miss_neg_ok) # 抵消过滤既有语义:封印后被撤封印不判(发射面基线过滤)——由排雷玩法 冒烟与封印判定基线覆盖,此处核兑付行零泄漏 var seal_srcs := {} for r in probe["rows"]: if String(r.get("source_class", "")) in ["seal_hit", "seal_miss"]: seal_srcs[r.get("origin_cell")] = true Kit.check("封印判定兑付仅封印判定批两封印在案(撤封印抵消过滤零泄漏——%d 格)" % seal_srcs.size(), int(seal_srcs.size()) <= 2) # 极性路由探针驱动:标 1 面真雷封印(标中)+ 1 面安全格封印(标空)→ AutoPlayer 挖到战斗终局(多轮样本)。 # 收集全部 cell_revealed / seal_judged 事件与结算行;返回格子集合与封印格坐标。 # ---- 封印不可逆四情况冒烟(禁撤封印 + 挖封印格兑付改判)---- # 四情况:① 插危险格不动 = 标中奖励照常;② 插危险格再挖开 = 只受挖雷一层惩罚(mine_hit 照常)标中不发; # ③ 插安全格不动 = 标空惩罚照常;④ 插安全格再挖开 = 零惩罚零奖励(正向兑付作废、标空不判)。 # 拒绝面:seal_pull 出目录(out_of_catalog);重复封印仍 seal_present 拒绝。 # 场景 A(④ + 拒绝面):标安全格 → seal_pull 拒绝 → 重复封印拒绝 → 直接挖开 → 收轮。 # 场景 B(②):标危险格 → 直接挖开 → 收轮。 # 场景 C(①③):标安全格 + 标危险格 → 不挖收轮 → 标空/标中判定照常。 func _smoke_seal_irreversible_quadrants() -> void: # 判据出处: design/gdd/systems/排雷循环/排雷玩法.md:231 (封印不可逆四情况组) # 判据出处: design/gdd/systems/排雷循环/排雷玩法.md:231 (封印不可逆四情况组) print("--- 封印不可逆四情况(禁撤封印 + 挖封印格兑付改判)---") # 场景 A:安全格直挖(情况④)+ seal_pull/重复封印拒绝面 var rng_a = load("res://core/rng_service.gd").new() rng_a.set_master_seed(20260919) var settle_a = load("res://core/battle_orchestrator.gd").new(_db, rng_a) var rows_a: Array = [] settle_a.step_row.connect(func(row): rows_a.append(row)) Kit.check("四情况 A:开局受理", settle_a.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"})) var ms_a = settle_a.minesweeper var t_a := _find_seal_target(ms_a, false) Kit.check("四情况 A:找到安全格封印目标", not t_a.is_empty()) if not t_a.is_empty(): if t_a.hero_cell != ms_a.hero_pos(): ms_a.settle_batch([{"action": "move", "cell": t_a.hero_cell}]) Kit.check("四情况 A:封印受理", ms_a.settle_batch([{"action": "seal_place", "cell": t_a.seal_cell}])[0].accepted) var pull_out: Array = ms_a.settle_batch([{"action": "seal_pull", "cell": t_a.seal_cell}]) Kit.check("四情况 A:seal_pull 出目录拒绝(撤封印退役)", not pull_out.is_empty() and not bool(pull_out[0].get("accepted", true)) and String(pull_out[0].get("sub_reason", "")) == "out_of_catalog") var dup_place: Array = ms_a.settle_batch([{"action": "seal_place", "cell": t_a.seal_cell}]) Kit.check("四情况 A:重复封印仍被拒(seal_present)", not dup_place.is_empty() and not bool(dup_place[0].get("accepted", true)) and String(dup_place[0].get("sub_reason", "")) == "seal_present") var dig_a: Array = ms_a.settle_batch([{"action": "dig", "cell": t_a.seal_cell}]) Kit.check("四情况 A:封印格直接挖开被受理(不再须先撤封印)", not dig_a.is_empty() and bool(dig_a[0].get("accepted", false))) Kit.check("四情况 A:封印随挖掘隐式退役(has_seal = false)", not ms_a.has_seal(t_a.seal_cell)) ms_a.settle_batch([{"action": "stop"}]) var target_a: Array = [t_a.seal_cell.x, t_a.seal_cell.y] var miss_a := 0 var grant_a := 0 var void_a := 0 for row in rows_a: if row.get("origin_cell", [0, 0]) != target_a: continue if row.get("type", "") == "seal_miss" or String(row.get("source_class", "")) == "seal_miss": miss_a += 1 if String(row.get("source_class", "")) == "safe_cell" and row.get("type", "") != "reward_ineffective": grant_a += 1 if row.get("type", "") == "reward_ineffective": void_a += 1 Kit.check("四情况 A:标空判定零兑付(×%d——封印已擦除不入轮末基线)" % miss_a, miss_a == 0) Kit.check("四情况 A:安全格正向兑付作废(×%d——零奖励)" % grant_a, grant_a == 0) Kit.check("四情况 A:兑付作废归因行恰一次(×%d——零惩罚零奖励留痕)" % void_a, void_a == 1) # 场景 B:危险格直挖(情况②) var rng_b = load("res://core/rng_service.gd").new() rng_b.set_master_seed(20260919) var settle_b = load("res://core/battle_orchestrator.gd").new(_db, rng_b) var rows_b: Array = [] settle_b.step_row.connect(func(row): rows_b.append(row)) Kit.check("四情况 B:开局受理", settle_b.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"})) var ms_b = settle_b.minesweeper var t_b := _find_seal_target(ms_b, true) Kit.check("四情况 B:找到危险格封印目标", not t_b.is_empty()) if not t_b.is_empty(): if t_b.hero_cell != ms_b.hero_pos(): ms_b.settle_batch([{"action": "move", "cell": t_b.hero_cell}]) Kit.check("四情况 B:封印受理", ms_b.settle_batch([{"action": "seal_place", "cell": t_b.seal_cell}])[0].accepted) var dig_b: Array = ms_b.settle_batch([{"action": "dig", "cell": t_b.seal_cell}]) Kit.check("四情况 B:封印危险格直接挖开被受理", not dig_b.is_empty() and bool(dig_b[0].get("accepted", false))) Kit.check("四情况 B:危险格已揭示(挖雷本体照常)", ms_b.is_revealed(t_b.seal_cell) and ms_b.current_board().is_mine(t_b.seal_cell)) ms_b.settle_batch([{"action": "stop"}]) var target_b: Array = [t_b.seal_cell.x, t_b.seal_cell.y] var mine_hit_b := 0 var hit_b := 0 for row in rows_b: if row.get("origin_cell", [0, 0]) != target_b: continue if String(row.get("source_class", "")) == "mine_hit": mine_hit_b += 1 if row.get("type", "") == "seal_hit" or String(row.get("source_class", "")) == "seal_hit": hit_b += 1 Kit.check("四情况 B:挖雷惩罚照常兑付(×%d——只受挖雷一层惩罚)" % mine_hit_b, mine_hit_b >= 1) Kit.check("四情况 B:标中不发(×%d——封印已擦除不判)" % hit_b, hit_b == 0) # 场景 C:不挖收轮(①③判定照常) var rng_c = load("res://core/rng_service.gd").new() rng_c.set_master_seed(20260919) var settle_c = load("res://core/battle_orchestrator.gd").new(_db, rng_c) var rows_c: Array = [] settle_c.step_row.connect(func(row): rows_c.append(row)) Kit.check("四情况 C:开局受理", settle_c.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"})) var ms_c = settle_c.minesweeper var t_safe := _find_seal_target(ms_c, false) var t_mine := _find_seal_target(ms_c, true) Kit.check("四情况 C:安全格+危险格封印目标齐备", not t_safe.is_empty() and not t_mine.is_empty()) if not t_safe.is_empty() and not t_mine.is_empty(): for t in [t_safe, t_mine]: if t.hero_cell != ms_c.hero_pos(): ms_c.settle_batch([{"action": "move", "cell": t.hero_cell}]) ms_c.settle_batch([{"action": "seal_place", "cell": t.seal_cell}]) ms_c.settle_batch([{"action": "stop"}]) # 不挖收轮——两封印均在场基线 var safe_c: Array = [t_safe.seal_cell.x, t_safe.seal_cell.y] var mine_c: Array = [t_mine.seal_cell.x, t_mine.seal_cell.y] var miss_c := 0 var miss_neg_c := 0 var hit_c := 0 for row in rows_c: if row.get("origin_cell", [0, 0]) == safe_c and row.get("type", "") == "seal_miss": miss_c += 1 if row.get("origin_cell", [0, 0]) == safe_c and String(row.get("source_class", "")) == "seal_miss" and row.get("type", "") != "seal_miss": miss_neg_c += 1 if row.get("origin_cell", [0, 0]) == mine_c and (row.get("type", "") == "seal_hit" or String(row.get("source_class", "")) == "seal_hit"): hit_c += 1 Kit.check("四情况 C:标空惩罚照常(归因×%d 负向兑付×%d)" % [miss_c, miss_neg_c], miss_c >= 1 and miss_neg_c >= 1) Kit.check("四情况 C:标中奖励照常(×%d)" % hit_c, hit_c >= 1) # ---- 试玩日志器冒烟(结算快照 JSONL + 诊断转储)---- # 负向字典逐型直验(类型化兑付行协议):找真实板上各型非封锁格,直调 grant_routed 负向路由(att 归因束形态), # 断言每型的兑付形态(enrage/off_guard/sapped 效果行、heal 直伤行、blast 警觉行、action 行动点排队)。 func _smoke_negative_dictionary() -> void: # 判据出处: design/gdd/systems/排雷循环/行动经济.md:256 (下轮行动点修正排队与兑现组) # 判据出处: design/gdd/systems/排雷循环/行动经济.md:256 (下轮行动点修正排队与兑现组) print("--- 负向字典逐型直验(sapped 在位 / heal 直伤 / blast 警觉 / action 行动点)---") # 数据面:sapped 行在位(状态表七行 + effect_pool_map 映射在位) var sapped: Dictionary = _db.combat.status_effects.row_by_id("sapped") Kit.check("sapped 行在位(hero · −25 percent · dur 1 · max 16 · stack——weaken 型负向)", not sapped.is_empty() and String(sapped.get("target_side", "")) == "hero" \ and int(sapped.get("magnitude", 0)) == -25 and int(sapped.get("duration_settlements", 0)) == 1 \ and int(sapped.get("max_stack", 0)) == 16 and String(sapped.get("stack_policy", "")) == "stack") Kit.check("effect_pool_map 收编 sapped → hero_offense(attack_up 镜像池)", String(_db.combat.settlement.core.effect_pool_map.get("sapped", "")) == "hero_offense") # 行为面:逐型负向兑付形态(真实板 + 真实路由路径) # heal 权重低(无 heal 板概率 ≈1/3)——单板五型齐备不再必然: # 本断言多板扫描,取第一块五型齐备的板做行为断言。 var settlement = null var board = null var by_type := {} # cell_type → 首个非封锁格(query_identity 雷安同池——危险格可查) var rows: Array = [] for seed_i in range(6): # 每板独立 rng 实例(复用实例重播种触发 set_master_seed 的 _counter==0 守卫; # 单板 break 会掩盖该违例,多板扫描才暴露) var rng = load("res://core/rng_service.gd").new() rng.set_master_seed(20260919 + seed_i) settlement = load("res://core/battle_orchestrator.gd").new(_db, rng) rows = [] settlement.step_row.connect(func(row): rows.append(row)) if not settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp), "hero_max_hp": int(_db.combat.hero.max_hp)}): continue board = settlement.minesweeper.current_board() var r0 := {} for c in board.compute_r0(): r0[c] = true by_type = {} for y in board.height: for x in board.width: var c := Vector2i(x, y) if r0.has(c) or by_type.size() == 5: continue var ident = settlement.query_reward_identity(c) # aux 身份不入五型轴(五型齐备=格面资源断言轴, # aux 供给挤占不改变本断言语义;扫描排除 aux 身份格) if ident != null and String(ident.cell_type) != RewardTable.TREASURE_ITEM_TYPE and not by_type.has(String(ident.cell_type)): by_type[String(ident.cell_type)] = c # break 条件=percent_map 三型齐备(aux 供给挤占下 weaken/defense 收集时机漂移, # 直验可达性优先;五型多样性 check 保留不动) if by_type.has("attack") and by_type.has("defense") and by_type.has("weaken"): break Kit.check("扫描 6 板内出现五型齐备板(%s)" % str(by_type.keys()), by_type.size() == 5) # attack/defense/weaken:负向字典行走 apply_effect(off_guard 走 ac 轴——兑付形态不变) var percent_map := {"attack": "enrage", "defense": "off_guard", "weaken": "sapped"} for ctype in percent_map.keys(): if not by_type.has(ctype): continue rows.clear() var snap: Dictionary = settlement._reward_economy.query_full_snapshot(by_type[ctype]) settlement.reward_grant().grant_routed(snap, "negative", {"source_class": "mine_hit", "cell": by_type[ctype], "ri": 1}) var eff: String = percent_map[ctype] Kit.check("%s 型负向 → %s 效果行(mine_hit 来源指回该格)" % [ctype, eff], _has_source_row(rows, "effect", "mine_hit") and _has_effect_id(rows, eff)) # heal:负向 = 主角直伤(表值;不经增益状态) if by_type.has("heal"): rows.clear() var hp_before: int = int(settlement.presentation_snapshot().hero.hp) var snap_h: Dictionary = settlement._reward_economy.query_full_snapshot(by_type["heal"]) settlement.reward_grant().grant_routed(snap_h, "negative", {"source_class": "mine_hit", "cell": by_type["heal"], "ri": 1}) var hp_after: int = int(settlement.presentation_snapshot().hero.hp) var backlash_power: int = 0 for row in _db.run.reward.rows: if String(row.get("reward_id", "")) == "heal_backlash": backlash_power = int(row.power) Kit.check("heal 型负向 → 主角直伤 %d(%d → %d,不经增益状态)" % [backlash_power, hp_before, hp_after], hp_after == hp_before - backlash_power) # blast:负向 = 警觉(敌方护甲等级 +1 percent 行) if by_type.has("blast"): rows.clear() var snap_b: Dictionary = settlement._reward_economy.query_full_snapshot(by_type["blast"]) settlement.reward_grant().grant_routed(snap_b, "negative", {"source_class": "mine_hit", "cell": by_type["blast"], "ri": 1}) var alert_ok: bool = false for eff_a in settlement._status_effects.query_active_side("enemy"): if String(eff_a.effect_id) == "alert": alert_ok = true Kit.check("blast 型负向 → 警觉(敌方护甲等级 +1 状态在位)", alert_ok) # action:负向 = 下轮行动点 −1 / 正向 = +1(排队,轮开始兑现;确定性直验不依赖板面摇号) rows.clear() var d0: int = settlement.action_economy.pending_ap_delta() settlement.reward_grant().grant_routed({"reward_id": "ap_drain", "cell_type": "action", "polarity": "negative", "display_name": "迟滞", "power": 1}, "negative", {"source_class": "mine_hit", "cell": Vector2i(0, 0), "ri": 1}) Kit.check("action 型负向 → 下轮行动点 −1(排队)", settlement.action_economy.pending_ap_delta() == d0 - 1) settlement.reward_grant().grant_routed({"reward_id": "ap_up", "cell_type": "action", "polarity": "positive", "display_name": "行动点", "power": 1}, "positive", {"source_class": "safe_open", "cell": Vector2i(0, 0), "ri": 1}) Kit.check("action 型正负抵消 → 排队净值归零", settlement.action_economy.pending_ap_delta() == d0) # 资源面断言:chain/dig 事件恒携带 resource=true(危险格同权);initial 恒 false(封锁格); # 封锁格查询 = null、危险格未开恒可查。 # 资源面断言:chain/dig 事件恒携带 resource=true(危险格同权);initial 恒 false(封锁格); # 封锁格查询 = null、危险格未开恒可查。 func _smoke_resource_surface() -> void: # 判据出处: design/gdd/systems/战斗循环/地图战斗资源.md:235 (资源面只读查询组) # 判据出处: design/gdd/systems/战斗循环/地图战斗资源.md:235 (资源面只读查询组) print("--- 格面资源资格面(危险格携带 / 封锁格无资源)---") var run1 = _run_battle(1, 0) var run2 = _run_battle(2, 0) var all_events: Array = run1.events + run2.events var digchain_bad := 0 var initial_bad := 0 var mine_with_resource := 0 var initial_count := 0 for evt in all_events: if not evt.has("resource"): continue # 非揭示事件(round_end 标记等) if String(evt.get("channel", "")) == "initial": initial_count += 1 if bool(evt.resource): initial_bad += 1 else: if not bool(evt.resource): digchain_bad += 1 if evt.kind == "mine": mine_with_resource += 1 Kit.check("chain/dig 揭示事件 resource 恒 true(雷安同池——违例 %d)" % digchain_bad, digchain_bad == 0) Kit.check("initial 揭示事件(封锁格)resource 恒 false(×%d 事件)" % initial_count, initial_bad == 0 and initial_count >= 1) Kit.check("危险格揭示携带资源(mine ×resource=true %d——危险格不再恒无)" % mine_with_resource, mine_with_resource >= 1) # 封锁格无身份 / 危险格未开恒可查(未开格恒可查——秘宝 桥接结构前提) var rng3 = load("res://core/rng_service.gd").new() rng3.set_master_seed(20260920) var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng3) settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)}) var board = settlement.minesweeper.current_board() var r0_first: Variant = null for c in board.compute_r0(): r0_first = c break var mine_unrevealed = null for c in board.mines.keys(): if not settlement.minesweeper.is_revealed(c): mine_unrevealed = c break Kit.check("封锁格(R₀)身份查询 = null(不入摇号清单)", settlement.query_reward_identity(r0_first) == null) Kit.check("未开危险格身份恒可查({reward_id, cell_type, tier}——秘宝 桥接前提)", mine_unrevealed != null and settlement.query_reward_identity(mine_unrevealed) != null) # 五段全序断言(战斗结算 验收"五段全序"+ 行型封闭 10 类):逐轮行相位序 = 授予 → 增益/减益 → 战斗 → 衰减 → 判定。 # ---- G8 reward 行读值改断(迭代 55「QA覆盖余量收口批」T8/V12——生产行驱动兑付链,替代内联合成行主证)---- # 现役断言(上方 action 型负向段)为内联合成行——行值变更零失败;本组读生产 reward 行对象走同一兑付链, # 断言真值依赖生产行值(行值变更即红)。合成行旧断言保留并存(零删除)。 func _check_enemy_alert_production_row() -> void: # 判据出处: design/engineering/test-conventions.md:133 (真实入口断言——生产行值驱动) var row: Dictionary = _db.run.reward.row_by_id("enemy_alert") Kit.check("enemy_alert 生产行值:blast 负向 effect=alert power0 weight0(不入池警觉行)〔QA余 V12〕", String(row.get("cell_type", "")) == "blast" and String(row.get("polarity", "")) == "negative" and String(row.get("effect_id", "")) == "alert" and int(row.get("power", -1)) == 0 and int(row.get("weight", -1)) == 0) var rg = load("res://core/rng_service.gd").new() rg.set_master_seed(20261108) var settlement = load("res://core/battle_orchestrator.gd").new(_db, rg) Kit.check("警觉生产行驱动:战斗启动", bool(settlement.begin_battle({"level_index": 1, "node_depth": 3, "hero_hp": int(_db.combat.hero.max_hp)}))) settlement.reward_grant().grant_routed(row, String(row.get("polarity", "negative")), {"source_class": "mine_hit", "cell": Vector2i(0, 0), "ri": 1}) var alert_ok := false for eff_a in settlement._status_effects.query_active_side("enemy"): if String(eff_a.effect_id) == String(row.get("effect_id", "")): alert_ok = true Kit.check("警觉兑付随生产行:effect_id=alert 状态在敌侧池(行 effect_id 变更即红)〔QA余 V12〕", alert_ok) func _check_ap_drain_production_row() -> void: # 判据出处: design/engineering/test-conventions.md:133 (真实入口断言——生产行值驱动) var row: Dictionary = _db.run.reward.row_by_id("ap_drain") Kit.check("ap_drain 生产行值:action 负向 power=1 weight0〔QA余 V12〕", String(row.get("cell_type", "")) == "action" and String(row.get("polarity", "")) == "negative" and int(row.get("power", -1)) == 1 and int(row.get("weight", -1)) == 0) var rg = load("res://core/rng_service.gd").new() rg.set_master_seed(20261109) var settlement = load("res://core/battle_orchestrator.gd").new(_db, rg) Kit.check("迟滞生产行驱动:战斗启动", bool(settlement.begin_battle({"level_index": 1, "node_depth": 3, "hero_hp": int(_db.combat.hero.max_hp)}))) var d0: int = settlement.action_economy.pending_ap_delta() settlement.reward_grant().grant_routed(row, String(row.get("polarity", "negative")), {"source_class": "mine_hit", "cell": Vector2i(0, 0), "ri": 1}) Kit.check("迟滞兑付随生产行:排队增量==-row.power==-1(行 power 变更即红)〔QA余 V12〕", settlement.action_economy.pending_ap_delta() == d0 - int(row.get("power", 0)))