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

139 lines
7.2 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.
# VERTICAL SLICE - NOT FOR PRODUCTION
# 奖励未生效行 reason 结构化断言(结算期文字列表退役批 T2——日志零文案后未生效原因的分析判读面):
# 四类合法例外(满层拒收 / 满血治疗 / 无存活敌人 / 主角已倒下)的真实结算行各带结构化 reason 键,
# 取值逐一命中 full_layers / full_hp / no_enemy_alive / hero_down(分析工具按此四值分类——
# 旧 text 子串判读退役后的替代判据面)。
# 真实入口:真实板格身份快照(query_full_snapshot)→ grant_routed 正向路由 → 轮末结账;
# 两处受控例外直改(对照表申报:无公开 setter 可钉)——hero hp=0(倒下因)与敌 hp=1(先手清场因)。
# 运行:godot --headless --path game --script res://tests/unit/battle_settlement/reward_ineffective_reason_test.gd(expect 0)
extends SceneTree
const Kit := preload("res://tests/unit/test_kit.gd")
const ConfigDBScript := preload("res://core/config_db.gd")
const RewardTable := preload("res://data/schemas/reward_table.gd")
var _db
func _init() -> void:
# 判据出处: design/gdd/systems/战斗循环/战斗结算.md:80 奖励未生效行=满层拒收、满血治疗等合法例外一行
# 判据出处: design/gdd/systems/战斗循环/战斗结算.md:87 显示名等玩家可见文案是数据面字段(日志消费端零文案——reason 结构化替代)
_db = ConfigDBScript.new()
if not _db.load_tables("res://data/tables", true):
print("[reward_ineffective_reason_test] FAIL — 数值表加载失败")
quit(1)
return
_assert_four_reasons()
quit(Kit.finish("reward_ineffective_reason_test"))
func _orch(seed_v: int):
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(seed_v)
return load("res://core/battle_orchestrator.gd").new(_db, rng)
# 真实板扫描:取某 cell_type 的首个非封锁格身份快照(seal_polarity 负向直验同款形态)
func _snap_of_type(settlement, ctype: String):
var board = settlement.minesweeper.current_board()
var r0 := {}
for c in board.compute_r0():
r0[c] = true
for y in board.height:
for x in board.width:
var c := Vector2i(x, y)
if r0.has(c):
continue
var ident = settlement.query_reward_identity(c)
if ident != null and String(ident.cell_type) == ctype \
and String(ident.cell_type) != RewardTable.TREASURE_ITEM_TYPE:
return settlement._reward_economy.query_full_snapshot(c)
return null
func _ineffective_rows(rows: Array) -> Array:
var out: Array = []
for r in rows:
if String(r.get("type", "")) == "reward_ineffective":
out.append(r)
return out
# 四类 reason 逐一命中(断言前提=各类未生效行在案;reason 键取值=分析判读契约四值)
func _assert_four_reasons() -> void:
# 判据出处: design/gdd/systems/战斗循环/战斗结算.md:80 奖励未生效行四类合法例外
print("--- 奖励未生效 reason 四类逐一命中 ---")
# ---- ① full_hp:满血主角 + heal 型格正向(_flat_heal 满血分支即时兑付)----
var s1 = _orch(20261108)
var rows1: Array = []
s1.step_row.connect(func(row): rows1.append(row.duplicate(true)))
Kit.check("① 前提:战斗受理", s1.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)}))
var heal_snap = _snap_of_type(s1, "heal")
Kit.check("① 前提:heal 型格在板", heal_snap != null)
if heal_snap != null:
s1.reward_grant().grant_routed(heal_snap, "positive", {"source_class": "safe_cell", "cell": Vector2i(0, 0), "ri": 1})
var hits := 0
for r in _ineffective_rows(rows1):
if String(r.get("reason", "")) == "full_hp":
hits += 1
Kit.check("① 满血治疗行带 reason=full_hp(命中 %d 行)" % hits, hits >= 1)
# ---- ② hero_down:主角 hp=0(受控例外直改——无公开 setter)+ heal 正向 → 倒下分支 ----
var s2 = _orch(20261109)
var rows2: Array = []
s2.step_row.connect(func(row): rows2.append(row.duplicate(true)))
Kit.check("② 前提:战斗受理", s2.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)}))
var heal_snap2 = _snap_of_type(s2, "heal")
Kit.check("② 前提:heal 型格在板", heal_snap2 != null)
if heal_snap2 != null:
var hu = s2._enemy_system.hero_unit() # 受控例外:hero hp 直改(倒下因无公开写口)
hu["hp"] = 0
s2.reward_grant().grant_routed(heal_snap2, "positive", {"source_class": "safe_cell", "cell": Vector2i(0, 0), "ri": 1})
var hits2 := 0
for r in _ineffective_rows(rows2):
if String(r.get("reason", "")) == "hero_down":
hits2 += 1
Kit.check("② 主角已倒下行带 reason=hero_down(命中 %d 行)" % hits2, hits2 >= 1)
# ---- ③ full_layers:defense 型格负向 → off_guard(stack 策略 max 16)反复授予至满层 → 全体拒收行
# (满层拒收只发生在 stack 策略——accumulate 恒加层无拒收面;负向 mine_hit 路由=真实入口)----
var s3 = _orch(20261110)
var rows3: Array = []
s3.step_row.connect(func(row): rows3.append(row.duplicate(true)))
Kit.check("③ 前提:战斗受理", s3.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)}))
var def_snap = _snap_of_type(s3, "defense")
Kit.check("③ 前提:defense 型格在板", def_snap != null)
if def_snap != null:
var max_stack: int = int(_db.combat.status_effects.row_by_id("off_guard").get("max_stack", 16))
for i in range(max_stack + 3): # 堆满后再给 3 份——拒收段必然到达
s3.reward_grant().grant_routed(def_snap, "negative", {"source_class": "mine_hit", "cell": Vector2i(0, 0), "ri": 1})
var hits3 := 0
for r in _ineffective_rows(rows3):
if String(r.get("reason", "")) == "full_layers":
hits3 += 1
Kit.check("③ 满层拒收行带 reason=full_layers(off_guard max_stack=%d,命中 %d 行)" % [max_stack, hits3], hits3 >= 1)
# ---- ④ no_enemy_alive:敌 hp=1(受控例外直改)+ blast 正向单两张 defer → 轮末结账首单清场、次单无敌 ----
var s4 = _orch(20261111)
var rows4: Array = []
s4.step_row.connect(func(row): rows4.append(row.duplicate(true)))
Kit.check("④ 前提:战斗受理", s4.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)}))
var blast_snap = _snap_of_type(s4, "blast")
Kit.check("④ 前提:blast 型格在板", blast_snap != null)
if blast_snap != null:
for u in s4._enemy_system.query_alive("enemy"): # 受控例外:敌 hp 直改(先手清场因)
u["hp"] = 1
s4.reward_grant().grant_routed(blast_snap, "positive", {"source_class": "safe_cell", "cell": Vector2i(0, 0), "ri": 1})
s4.reward_grant().grant_routed(blast_snap, "positive", {"source_class": "safe_cell", "cell": Vector2i(0, 0), "ri": 1}) # 次单=结账时已无敌
s4.minesweeper.settle_batch([{"action": "stop"}]) # 轮末五段——战斗段末 defer 统一结账
var hits4 := 0
for r in _ineffective_rows(rows4):
if String(r.get("reason", "")) == "no_enemy_alive":
hits4 += 1
Kit.check("④ 无存活敌人行带 reason=no_enemy_alive(命中 %d 行)" % hits4, hits4 >= 1)