# Conflicts: # game/tests/unit/battle_settlement/roll_multiplier_test.gd # game/tests/unit/core/adventure_feats_test.gd # game/tests/unit/core/config_db_resolver_test.gd # game/tests/unit/core/level_shop_economy_test.gd # game/tests/unit/core/save_boot_no_activate_test.gd
184 lines
13 KiB
GDScript
184 lines
13 KiB
GDScript
# tests/integration/cross_system_flow_test.gd — L2 集成用例:跨系统真实流×落盘(批 B 转办块宿主)
|
||
# 断言面 = 四块 22 条(批 B 断言迁移自 smoke_all.gd,主会话裁决一/二/三/七改判集成,装配批落位;
|
||
# 标签与 cond 逐字搬运,宿主通道换线 Kit.check——与单元侧同一对账口径):
|
||
# RNG 事件日志会话落盘 1 条(源 `_initialize` :70,裁决一——RngService 会话日志×磁盘)
|
||
# 专长断点往返 3 条(源 `_smoke_feats` 基点 :1420–1426,裁决二——AdventureRunner 兑付态×SaveManager 断点快照)
|
||
# 存档摘要集成 10 条(源 `_smoke_save` ⑨ 基点 :6807–6831,裁决三——level_autoplay 真局×SaveManager 入账×写盘读回)
|
||
# 试玩记录仪 8 条(源 `_smoke_playtest_logger` 整套,裁决七——PlaytestLogger×BattleOrchestrator 真实结算流×JSONL 落盘)
|
||
# 协议照集成侧惯例(与单元同入口同协议,tests/README「测试层级」):extends SceneTree+Kit.check 主通道;
|
||
# --script 模式无 autoload,ConfigDB/RngService 显式实例化构造注入;表加载失败不中止(沿被迁断言面的
|
||
# 源语义——诊断在此展开,红由具体断言落,承 G2–G5 同款);冒烟库件迁移期按现路径 preload(账本 §五夹具前提)。
|
||
# 驱动代码随条自持、零新增断言(验证集不扩大为绝对约束——前置受理失败自然落到后续断言红)。
|
||
# 运行:godot --headless --path game --script res://tests/integration/cross_system_flow_test.gd
|
||
extends SceneTree
|
||
|
||
const Kit := preload("res://tests/unit/test_kit.gd")
|
||
const ConfigDBScript := preload("res://core/config_db.gd")
|
||
const LevelAutoplay := preload("res://sim/level_autoplay.gd") # 真局驱动(迁移期按现路径 preload——账本 §五夹具前提)
|
||
|
||
var _db
|
||
|
||
|
||
func _init() -> void:
|
||
_db = ConfigDBScript.new()
|
||
# 表加载失败不中止(承 G2–G5 同款:表损坏的红落在具体断言上)
|
||
var load_ok: bool = _db.load_tables("res://data/tables", true)
|
||
if not load_ok:
|
||
push_error("[cross_system_flow_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])
|
||
# 套执行序沿源编排(_initialize 尾条 → _smoke_feats 断点块 → _smoke_save ⑨ → _smoke_playtest_logger)
|
||
_rng_event_log_session()
|
||
_feats_breakpoint_roundtrip()
|
||
_save_summary_integration()
|
||
_smoke_playtest_logger()
|
||
quit(Kit.finish("cross_system_flow_test"))
|
||
|
||
|
||
# 冒烟隔离用全新 RngService(同 master 种子——回放确定性纪律;smoke_all.gd helper 的宿主自持私有副本)
|
||
func _fresh_rng(seed_val: int):
|
||
var rng = load("res://core/rng_service.gd").new()
|
||
rng.set_master_seed(seed_val)
|
||
return rng
|
||
|
||
|
||
# ---- ① RNG 事件日志会话落盘(源 `_initialize` :70,主会话裁决一改判集成——
|
||
# 驱动沿源 :60–70 取号序列自持:master 种子 42 → 前置取号 → new_adventure 开日志会话 → 会话内取号)----
|
||
func _rng_event_log_session() -> void:
|
||
var rng = load("res://core/rng_service.gd").new()
|
||
rng.set_master_seed(42)
|
||
rng.open_stream("board_gen")
|
||
rng.open_stream("encounter")
|
||
rng.new_adventure(4242)
|
||
rng.open_stream("gp_drop")
|
||
Kit.check("事件日志会话落盘(2026-09-27 统一流裁决)", FileAccess.file_exists("user://logs/rng_log_4242.jsonl"))
|
||
|
||
|
||
# ---- ② 专长断点往返(源 `_smoke_feats` 基点 :1420–1426 三条,主会话裁决二改判集成——
|
||
# 驱动沿源 :721–797 重建:5 行临时表注入+seed 72 起局+兑付 x3+save_manager 装配+
|
||
# _on_level_finished(victory)+breakpoint_snapshot 取读+clear_breakpoint;
|
||
# 起局与兑付两步的受理断言留单元宿主(adventure_feats_test),此处纯驱动——前置失败自然落红)----
|
||
func _feats_breakpoint_roundtrip() -> void:
|
||
var rt := FeatTable.new()
|
||
rt.rows = [
|
||
{"feat_id": "x1", "display_name": "试一", "rarity": "common", "desc": "测试揭示", "cap": 1,
|
||
"triggers": [{"on": "mine_hit", "do": {"kind": "reveal_tiles", "value": 1}}]},
|
||
{"feat_id": "x2", "display_name": "试二", "rarity": "excellent", "desc": "测试回复", "cap": 1,
|
||
"triggers": [{"on": "mine_hit", "do": {"kind": "heal", "value": 2}}]},
|
||
{"feat_id": "x3", "display_name": "试三", "rarity": "rare", "desc": "测试群伤", "cap": 1,
|
||
"triggers": [{"on": "seal_hit", "do": {"kind": "open_ticket", "channel": "blast", "value": 3}}]},
|
||
{"feat_id": "x4", "display_name": "试四", "rarity": "rare", "desc": "测试减伤", "cap": 2,
|
||
"triggers": [{"on": "damage_taken", "when": [{"fact": "hp_ratio", "op": "lte", "value": 50}], "do": {"kind": "damage_reduce", "value": 2}}]},
|
||
{"feat_id": "x5", "display_name": "试五", "rarity": "legendary", "desc": "测试AC", "cap": 1,
|
||
"triggers": [{"on": "seal_hit", "do": {"kind": "ac_bonus", "value": 1, "mode": "round"}}]},
|
||
]
|
||
var live_rows: FeatTable = _db.crossrun.feats
|
||
_db.crossrun.feats = rt
|
||
var a_r = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(72))
|
||
a_r.start_adventure(72) # 源 :723(受理断言留单元宿主)
|
||
a_r.stop()._pending = {"candidates": [{"kind": "relic", "tier": "rare", "feat_id": "x3"}, {"kind": "gp", "tier": "common"}], "payload": {}}
|
||
a_r.stop().pick_curio("relic", "rare", "x3") # 源 :781(兑付断言留单元宿主)
|
||
# 断点往返(持有集+余库 feat_id 随行)
|
||
var bp_path := "user://_smoke_feats_bp.json" # 段首清场(存档域隔离纪律——上轮残留断点先删)
|
||
if FileAccess.file_exists(bp_path):
|
||
DirAccess.remove_absolute(bp_path)
|
||
var sm_r = load("res://core/save_manager.gd").new(bp_path)
|
||
sm_r.load_from_disk()
|
||
a_r.set_save_manager(sm_r)
|
||
a_r._on_level_finished({"result": "victory", "hp": int(a_r.stats().hero_hp), "node_index": 1, "node_id": 1, "node_type": "shop"})
|
||
var bp_r: Dictionary = sm_r.breakpoint_snapshot()
|
||
Kit.check("断点附随件:relics 持有集入快照(迭代 12 T1 扩列)", (bp_r.get("relics", []) as Array).has("x3"))
|
||
Kit.check("断点附随件:adventure 计数器账本键在案(T6/T7 扩列)", bp_r.has("adventure_counters"))
|
||
var bp_deck_x3 := 0
|
||
for t in (bp_r.get("bonus_deck", []) as Array):
|
||
if String((t as Dictionary).get("feat_id", "")) == "x3":
|
||
bp_deck_x3 += 1
|
||
Kit.check("断点余库:已兑付 relic 令牌逐枚还原(x3 零残留——续玩不重复获得)", bp_deck_x3 == 0)
|
||
sm_r.clear_breakpoint()
|
||
if FileAccess.file_exists(bp_path): # 段尾清理(与段首清场对称)
|
||
DirAccess.remove_absolute(bp_path)
|
||
_db.crossrun.feats = live_rows # 还原现行表(共享 _db 防污染——后续集成块零感知)
|
||
|
||
|
||
# ---- ③ 存档摘要集成(源 `_smoke_save` ⑨ 基点 :6807–6831 十条,主会话裁决三改判集成——
|
||
# level_autoplay 真局摘要 → apply 原语 → 域读数 → 写盘读回保持;临时档 user://smoke_save_integration.json
|
||
# 段首清场+段尾清理(源 _smoke_save 隔离纪律),绝不碰 user://save.json(红线))----
|
||
func _save_summary_integration() -> void:
|
||
var sm_script = load("res://core/save_manager.gd")
|
||
var path9 := "user://smoke_save_integration.json" # 源 paths[8]
|
||
if FileAccess.file_exists(path9):
|
||
DirAccess.remove_absolute(path9)
|
||
# ⑨ 摘要集成(冒险结构批产物驱动):run_adventure 真局摘要 → apply 原语 → 域读数 → 写盘读回保持
|
||
var st9: Dictionary = LevelAutoplay.run_adventure(_db, 42, 900, 400)
|
||
var f9 = sm_script.new(path9)
|
||
f9.meta_state.apply_adventure_summary(st9.get("summary", {}))
|
||
var ks_run := 0
|
||
for li in range(1, int(_db.run.level.rows.size()) + 1):
|
||
ks_run += int(_db.crossrun.meta.boss_keystone_row(li).get("first_kill", 0))
|
||
Kit.check("摘要集成:冒险驱动满配冒险成功(seed 42)", String(st9.get("result", "")) == "completed")
|
||
Kit.check("集成钥石余额 = ΣBoss 首杀行值(%d——逐杀掉落全源)" % f9.meta_state.keystone_balance(), f9.meta_state.keystone_balance() == ks_run)
|
||
Kit.check("集成关卡完成次数表全 1", f9.meta_state.career_state().clear_counts.get(str(1), 0) == 1 and f9.meta_state.career_state().clear_counts.get(str(2), 0) == 1 and f9.meta_state.career_state().clear_counts.get(str(3), 0) == 1)
|
||
Kit.check("集成三关首杀标记齐", f9.meta_state.career_state().boss_first_kill.has(str(1)) and f9.meta_state.career_state().boss_first_kill.has(str(2)) and f9.meta_state.career_state().boss_first_kill.has(str(3)))
|
||
Kit.check("集成 recent 1 条且 seed 42", f9.meta_state.career_state().recent_summaries.size() == 1 and int(f9.meta_state.career_state().recent_summaries[0].get("master_seed", -1)) == 42)
|
||
var st9b: Dictionary = LevelAutoplay.run_adventure(_db, 42, 900, 400)
|
||
f9.meta_state.apply_adventure_summary(st9b.get("summary", {}))
|
||
Kit.check("第二局后关卡完成次数全 2", f9.meta_state.career_state().clear_counts.get(str(1), 0) == 2 and f9.meta_state.career_state().clear_counts.get(str(2), 0) == 2 and f9.meta_state.career_state().clear_counts.get(str(3), 0) == 2)
|
||
Kit.check("第二局后余额翻倍(2 × Σ首杀——模拟驱动恒首杀口径)", f9.meta_state.keystone_balance() == 2 * ks_run)
|
||
Kit.check("集成 recent 长度 2", f9.meta_state.career_state().recent_summaries.size() == 2)
|
||
Kit.check("集成写盘成功", f9.write_to_disk())
|
||
var r9 = sm_script.new(path9)
|
||
Kit.check("集成写盘读回全量保持", r9.load_from_disk() and r9.meta_state.keystone_balance() == 2 * ks_run and r9.meta_state.career_state().clear_counts.get(str(3), 0) == 2
|
||
and r9.meta_state.career_state().recent_summaries.size() == 2 and r9.meta_state.career_state().boss_first_kill.has(str(3)))
|
||
if FileAccess.file_exists(path9): # 段尾清理(与段首清场对称——源 _smoke_save 同款)
|
||
DirAccess.remove_absolute(path9)
|
||
|
||
|
||
# ---- ④ 试玩记录仪(源 `_smoke_playtest_logger` 整套八条,主会话裁决七改判集成——
|
||
# PlaytestLogger 挂真实结算流:会话文件/逐结算快照/字段齐备/spawn_count 回归/诊断转储/JSONL 回读)----
|
||
func _smoke_playtest_logger() -> void:
|
||
print("--- PlaytestLogger(结算快照 JSONL + 诊断转储)---")
|
||
var rng = load("res://core/rng_service.gd").new()
|
||
rng.set_master_seed(20260912)
|
||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
|
||
var logger = load("res://presentation/playtest_logger.gd").new()
|
||
var path: String = logger.start_session(20260912)
|
||
logger.attach_battle(settlement)
|
||
var snaps: Array = []
|
||
settlement.settlement_snapshot.connect(func(snap): snaps.append(snap))
|
||
Kit.check("会话文件开启(%s)" % path, path.contains("playtest_20260912"))
|
||
Kit.check("快照战斗启动", settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)}))
|
||
var minesweeper = settlement.minesweeper
|
||
for i in range(2):
|
||
minesweeper.settle_batch([{"action": "stop"}])
|
||
Kit.check("每结算一快照(%d 次结算 → %d 快照)" % [2, snaps.size()], snaps.size() == 2)
|
||
var s1: Dictionary = snaps[0]
|
||
var required := ["kind", "level", "depth", "round", "verdict", "hero", "economy", "board", "enemies", "buffs", "round_facts", "spawn_count"]
|
||
var missing := []
|
||
for k in required:
|
||
if not s1.has(k):
|
||
missing.append(k)
|
||
Kit.check("快照字段齐备(缺 %s)" % str(missing), missing.is_empty())
|
||
Kit.check("快照含英雄/经济/板序号(板 seq=%d)" % int(s1.board.seq), int(s1.hero.max_hp) == int(_db.combat.hero.max_hp) and s1.economy.has("phase") and s1.board.has("seq"))
|
||
# spawn_count 回归(playtest-2026-09-12 报告 Bug#1):停手两轮零击杀(主角攻击 6×2 < 24 HP 确定)→
|
||
# unit_count 必须 == spawn_count(全灭才允许 <);快照级区分「首轮全灭」vs「空场」的数据面在位
|
||
Kit.check("快照 spawn_count 在位且 == unit_count(无击杀轮:%d/%d——全灭/空场可判别)" % [int(s1.get("spawn_count", -1)), int(s1.get("unit_count", -1))],
|
||
int(s1.get("spawn_count", -1)) >= 1 and int(s1.get("spawn_count", -1)) == int(s1.get("unit_count", -2)))
|
||
logger.log_action("dig", Vector2i(1, 1), true)
|
||
var diag: String = logger.dump_diagnosis({"test": true})
|
||
Kit.check("诊断转储文件生成(%s)" % diag, diag.contains("diag_"))
|
||
# JSONL 文件内容验证(重新读取)
|
||
var f := FileAccess.open(path, FileAccess.READ)
|
||
var lines := 0
|
||
var parse_ok := true
|
||
while not f.eof_reached():
|
||
var line := f.get_line()
|
||
if line.strip_edges() == "":
|
||
continue
|
||
lines += 1
|
||
var parsed = JSON.parse_string(line)
|
||
if parsed == null:
|
||
parse_ok = false
|
||
Kit.check("JSONL 全部行可解析(%d 行 = session + 2 结算)" % lines, lines >= 3 and parse_ok)
|
||
logger.close_session()
|