test(批B): 断言层级重构大提交——1065 条断言自 smoke_all.gd 全量迁入 16 标准宿主(冒烟/单元/集成/系统+UI 探针);smoke_all.gd 整体退役;变异引擎按用户令删除、验证改核代码(三冷启动审核员 1065 全审:1047 有效,17 存疑+2 段落级,其中 2 段落级经今日闸记录实证解决);10 条核代码修复(恒真/名实/静默跳过/键名等——grilling 一轮修订);验证集清单回写 1068 条三对齐零差异;底册去向补正与笔误清账

This commit is contained in:
DKChan committed 2026-10-23 20:48:20 +08:00
1 parent 8fbd2dd875
commit 957391e13b
29 files changed
+6759 -8446

No files matched your search

@@ -0,0 +1,183 @@
# 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://smoke/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, {"first_script": true}) # 源 :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()
@@ -0,0 +1,107 @@
# tests/integration/node_map_templates_test.gd — L2 集成用例:关卡节点图生成·模板池
# 断言面 = 模板池套(批 B 断言迁移自 smoke_all.gd `_smoke_node_map_templates`,G12 施工组;
# 主会话裁决六整套集成不拆——跨四系统且走落盘)。
# 跨系统面 = RngService(node_map 用途标签/抽签确定性/零消耗计账)×
# LevelRunner(start_level_external 受理/模板 0 回滚档)×
# AdventureRunner(first_script 全关零抽签/逐关恰一抽签)×
# SaveManager+MetaProgression(首次冒险布尔位收束置位落盘——真实档写盘)。
# 与单元侧同入口同协议:extends SceneTree+Kit.check 主通道;--script 模式无 autoload,
# ConfigDB 显式实例化构造注入;表加载失败不中止(沿被迁断言面的源语义——诊断展开,红由断言落)。
# 运行:godot --headless --path game --script res://tests/integration/node_map_templates_test.gd
extends SceneTree
const Kit := preload("res://tests/unit/test_kit.gd")
var _db
func _init() -> void:
_db = load("res://core/config_db.gd").new()
# 表加载失败不中止(承 G2/G3/G4/G5 同款:表损坏的红落在具体断言上)
var load_ok: bool = _db.load_tables("res://data/tables", true)
if not load_ok:
push_error("[node_map_templates_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_node_map_templates()
quit(Kit.finish("node_map_templates_test"))
# ---- 关卡节点图生成·模板池冒烟(v0.4 §P0.6 断言面)----
func _smoke_node_map_templates() -> void:
# ① 扩签:node_map 入用途标签封闭词表(派生零改动——标签只进事件日志)
var svc = load("res://core/rng_service.gd").new()
Kit.check("node_map 标签入 rng_service 封闭词表(扩签通道=改代码+冒烟守卫)",
(svc.PURPOSE_TAGS as Array).has("node_map"))
# ② 池参数:三关各 5 模板、模板 0 存在(回滚档成员)
for li in range(1, 4):
var row := {}
for r in _db.level.rows:
if int(r.level_index) == li:
row = r
var tpls: Array = row.get("templates", [])
Kit.check("L%d 模板池 = 5 张(拍板点④——首版池规模)" % li, tpls.size() == 5)
Kit.check("L%d 模板 0 = 现行固定图(回滚档成员)" % li,
not tpls.is_empty() and String(tpls[0].get("display_name", "")).ends_with("(现行)"))
# ③ 强制取 0 = 模板 0 节点表逐位相等 + 零 scope 消费
var rng0 = load("res://core/rng_service.gd").new()
rng0.new_adventure(910001)
var runner0 = load("res://core/level_runner.gd").new(_db, rng0)
var c_before: int = rng0.counter()
Kit.check("强制 template_pick=0 起关受理", runner0.start_level_external(2, {"hero_hp": 20, "gp": 0}, {}, # start_level_external 签名收口(bonus 参数由 RunLedger 单类下传)
Callable(self, "_smoke_rest_stub"), {}, true, 0))
Kit.check("强制取模板零 RNG 消耗(零抽签——首次脚本语义)", rng0.counter() == c_before)
var tpl0: Dictionary = {}
for r in _db.level.rows:
if int(r.level_index) == 2:
tpl0 = r.get("templates", [])[0]
Kit.check("强制 0 的生效节点表 = 模板 0 逐位相等(回滚档成立)",
runner0.level_row.get("nodes", []) == tpl0.get("nodes", []))
# ④ 抽签确定性:同种子两实例同模板(同种子同操作序列 → 逐位同图)
var names := []
for i in range(2):
var rg = load("res://core/rng_service.gd").new()
rg.new_adventure(910002)
var rn = load("res://core/level_runner.gd").new(_db, rg)
rn.start_level_external(2, {"hero_hp": 20, "gp": 0}, {},
Callable(self, "_smoke_rest_stub"), {}, true, -1)
names.append(rn.active_template_name)
Kit.check("抽签确定性:同种子逐位同图(实测 %s)" % String(names[0]),
names[0] == names[1] and String(names[0]) != "")
# ⑤ N=1 池与现行固定图等价:单模板行过图校验(抽签取模 1 恒 0 = 回滚档)
var lt = load("res://data/schemas/level_table.gd").new()
lt.rows.assign([{"level_index": 9, "display_name": "t", "rest_heal_pct": 30,
"templates": [tpl0]}])
Kit.check("N=1 池表通过图校验(回滚档形态合法)", lt.validate().is_empty())
# ⑥ 首次脚本分支:first_script=true 全关模板 0;false = 抽签(scope 恰一差异)
var rngF = load("res://core/rng_service.gd").new()
rngF.new_adventure(910003)
var adrF = load("res://core/adventure_runner.gd").new(_db, rngF)
adrF.start_adventure(910003, {"first_script": true})
Kit.check("首次脚本模式:L1 取模板 0(全关零抽签——存档布尔位 false 语义)",
String(adrF.runner().active_template_name).ends_with("(现行)"))
var cF: int = rngF.counter()
var rngT = load("res://core/rng_service.gd").new()
rngT.new_adventure(910003)
var adrT = load("res://core/adventure_runner.gd").new(_db, rngT)
adrT.start_adventure(910003, {"first_script": false})
Kit.check("抽签模式恰多消费 1 scope(node_map 逐关恰一——L1 起关抽签)", rngT.counter() == cF + 1)
Kit.check("抽签模式 active_template_name 有值(MC 分层出数取数面在位)",
String(adrT.runner().active_template_name) != "")
# ⑦ 收束置位:新档 false → settle 置 true 恰一次(幂等)
DirAccess.remove_absolute("user://smoke_node_map_save.json") # 测试隔离——上轮 settle 写盘的残留档先删
var sv = load("res://core/save_manager.gd").new("user://smoke_node_map_save.json")
sv.load_from_disk()
Kit.check("新档首次冒险布尔位 = false(默认域)", sv.is_first_adventure_done() == false)
var mp = load("res://core/meta_progression.gd").new(_db, sv)
mp.settle_adventure({"levels_cleared": 1, "keystone_total": 10, "cleared": false, "result": "failed"})
Kit.check("冒险收束点置位(三结局同写——写入只在收束挂点)", sv.is_first_adventure_done() == true)
mp.settle_adventure({"levels_cleared": 0, "keystone_total": 0, "cleared": false, "result": "failed"})
Kit.check("置位幂等(重复收束不翻转)", sv.is_first_adventure_done() == true)
# ---- 冒烟恢复执行体(LevelRunner 注入用——与 AdventureRunner.apply_rest 同式占位;
# smoke_all.gd helper 的宿主自持私有副本——源文件收尾整体删除,宿主自给自足)----
func _smoke_rest_stub(pct: int, hp: int, max_hp: int) -> int:
return mini(int(floor(float(max_hp) * float(pct) / 100.0)), max_hp - hp)