Files
gplan-2/game/tests/unit/battle_settlement/seal_polarity_test.gd
T

873 lines
52 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)))