Files
gplan-2/game/tests/integration/cross_system_flow_test.gd
T
DKChan 9ee3bea194 refactor(迭代35卡②T2·二口+T3 S1/S2/S6/S8): 消费侧切源落定+断言面换代主体——① level_runner 图源换代(_generator 取代模板解析,_assign_generated_graph 读 level_gen_params 参数档产图并过 LevelTable.validate_graph,违规起关拒绝零兜底)+_entry_nodes 多入口首跳+_node_id 句柄单空间+enc node_depth 取节点 depth 值;② adventure_runner/main/save_manager 模板链路退役(first_script 参数与 template_pick 计算全删,断点 template_pick 键换生成图代差标记键 graph_gen_v2,缺键旧断点 resume 显式拒绝禁静默回放);③ level.tres 三行 templates 键清出(15 张模板数据出表,行级字段留,表尾注释改指参数档+生成器);④ sim/probe 随动(run_level/run_adventure 尾参退役、MC 出数字段 level_templates→level_maps 值=关卡#主种子、探针注释性质化、boss_depth 取节点 depth 键);⑤ 断言面换代:node_map_templates 宿主整段重写为生成图确定性套(纯函数复现/换种子变图/零计数器号位依赖/单一图源/种子 42 两开局同图/断点重生成同图/缺代差键拒绝/表无 templates/收束置位三条保留),breakpoint_resume 新增 P7 六断言(快照无已抽模板键、代差标记、停点句柄与已到序往返、续玩同图、旧断点拒绝、同种子重开局同图),S8 单测随动 first_script 键清退+断点夹具换代+meta 参数面 extra 键样换 treasure_unlocked(语义保持),S2 生成图取数面三宿主替换(level_active_nodes 走参数档+种子 42,消费点深度键换 depth,round_flow 死件 _boss_depth 随删),adventure_feats 覆盖义务段改真实解析链判据(敌侧落带≥请求深度+板侧命中+非空扫描面)、奖励节点段换「图侧含件+真实选路点选呈停兑付」双断言(旧单链必经形状前提随模板消亡,非放宽:经停即呈候选由真实 pick_next_node 入口证明)、商店余额段按每关恰一店投影改三关读数
2026-10-24 18:59:57 +08:00

184 lines
12 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/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.feats
_db.feats = rt
var a_r = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(72))
a_r.start_adventure(72) # 源 :723(受理断言留单元宿主)
a_r._curio_pending = {"candidates": [{"kind": "relic", "tier": "rare", "feat_id": "x3"}, {"kind": "gp", "tier": "common"}], "payload": {}}
a_r.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.ledger.get("hero_hp", 26)), "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.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.apply_adventure_summary(st9.get("summary", {}))
var ks_run := 0
for li in range(1, int(_db.level.rows.size()) + 1):
ks_run += int(_db.meta_boss_keystone_row(li).get("first_kill", 0))
Kit.check("摘要集成:冒险驱动满配通关(seed 42)", String(st9.get("result", "")) == "completed")
Kit.check("集成钥石余额 = ΣBoss 首杀行值(%d——逐杀掉落全源)" % f9.keystone_balance(), f9.keystone_balance() == ks_run)
Kit.check("集成通关次数表全 1", f9.clear_count(1) == 1 and f9.clear_count(2) == 1 and f9.clear_count(3) == 1)
Kit.check("集成三关首杀标记齐", f9.boss_first_kill_marked(1) and f9.boss_first_kill_marked(2) and f9.boss_first_kill_marked(3))
Kit.check("集成 recent 1 条且 seed 42", f9.recent_summaries().size() == 1 and int(f9.recent_summaries()[0].get("master_seed", -1)) == 42)
var st9b: Dictionary = LevelAutoplay.run_adventure(_db, 42, 900, 400)
f9.apply_adventure_summary(st9b.get("summary", {}))
Kit.check("第二局后通关次数全 2", f9.clear_count(1) == 2 and f9.clear_count(2) == 2 and f9.clear_count(3) == 2)
Kit.check("第二局后余额翻倍(2 × Σ首杀——模拟驱动恒首杀口径)", f9.keystone_balance() == 2 * ks_run)
Kit.check("集成 recent 长度 2", f9.recent_summaries().size() == 2)
Kit.check("集成写盘成功", f9.write_to_disk())
var r9 = sm_script.new(path9)
Kit.check("集成写盘读回全量保持", r9.load_from_disk() and r9.keystone_balance() == 2 * ks_run and r9.clear_count(3) == 2
and r9.recent_summaries().size() == 2 and r9.boss_first_kill_marked(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.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.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()