873 lines
52 KiB
GDScript
873 lines
52 KiB
GDScript
# 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)))
|