测试层合并批(用户裁 2026-11-07): 「集成测试」层级退役归并单元层——tests/integration/ 三套件(存档⇄RNG 往返/节点图确定性/跨系统流×落盘)实体迁移 tests/unit/ 同闸同协议, run_all manifest 65 套全量单元; 闸名「单元与集成测试」→「单元测试」(run_all_gates/基线键/coverage_receipts 同步); 选闸词 integration 族→save_rng/cross_system/node_map 文件名词; 测试层识别换三元组(职责×触发时间点×覆盖面积, 权威表=test-conventions §1.1 测试层三元组), 层级名降为代称; 术语表退役词对照+doc_term 收「集成测试/用例/层/侧/宿主/族」, 全仓旧字样清零(gdd/规约/README/套件头注/断言标签), 跨系统面性质归属由各套件头注承载; extract_checks 缺省遍历面收 unit/**; test-conventions §8.2 验收四层「集成」改「跨宿主」; architecture 测试层级表三级换代+65 实数; domain_route/AGENTS 字样同步。凭据: 三套件单跑 exit=0; --code 域闸验收通过(冒烟/单元全量/系统/场景探针/渲染闸/静态扫描/熵增闸全绿); --docs 域闸验收通过(文档域七闸零新增红)
This commit is contained in:
1 parent
40a3ac1281
commit
9f80609518
28 files changed
+139
-127
No files matched your search
@@ -659,7 +659,7 @@ func _smoke_skill_pool_data() -> void:
|
||||
if n_uncond < 1:
|
||||
uncond_ok = false
|
||||
Kit.check("每池至少一行无条件特殊技(触发条件批池构成纪律——逐池复查)〔触发器批 V7〕", uncond_ok)
|
||||
# Boss 半血解锁技·编排集成断言(真实编排回路:半血 → 下轮开轮闩检查 → 解锁行恰一次)
|
||||
# Boss 半血解锁技·编排回路断言(真实编排回路:半血 → 下轮开轮闩检查 → 解锁行恰一次)
|
||||
var st_unlock: Dictionary = {"rows": []}
|
||||
var sett_u = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(4242))
|
||||
sett_u.step_row.connect(func(row): st_unlock["rows"].append(row))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# 触发条件机制单测(技能池情境条件触发器批——V1 出域拒绝/V2 双面过滤/V3 保底不逼/V4 闩型语义/
|
||||
# V6 事实面供给/V7 池构成纪律/V8 低血防御/V9 停手偷袭/V10 缺省零扰动;V5 对拍由 skill_unlock_test
|
||||
# 迁移断言与 round_flow 编排集成断言承载)。
|
||||
# 迁移断言与 round_flow 编排回路断言承载)。
|
||||
# 运行:godot --headless --path game --script res://tests/unit/battle_settlement/trigger_cond_test.gd(expect 0)
|
||||
extends SceneTree
|
||||
|
||||
@@ -199,7 +199,7 @@ func _init() -> void:
|
||||
stray += 1
|
||||
Kit.check_quiet(stray == 0, "V10 无 trigger 行池快照零新键(快照键策略钉死——防未来给普行塞键)〔触发器批 V10〕")
|
||||
|
||||
quit(Kit.finish("trigger_cond_test", "PASS — 触发条件 V1/V2/V4/V6/V7/V8/V9/V10 全绿(V5 对拍由 skill_unlock/round_flow 编排集成承载)"))
|
||||
quit(Kit.finish("trigger_cond_test", "PASS — 触发条件 V1/V2/V4/V6/V7/V8/V9/V10 全绿(V5 对拍由 skill_unlock/round_flow 编排回路承载)"))
|
||||
|
||||
|
||||
func _rng(seed_val: int):
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# 冒险结构 API 闸(状态机受理面——重复开始拒绝/入口停点/放弃两态)
|
||||
# 冒险验收断言(三关序列/大奖励三层兑付/账本封闭/败北线/恢复数学/加算区同账/共享池)
|
||||
# 专长重设计实装(feat_table 四级/案D 池接线/V3 订阅面逐件三列/专长凭证保底)
|
||||
# 注:断点往返三条(基点 1420–1426)按主会话裁决②改判集成宿主,不在本宿主断言面内。
|
||||
# 注:断点往返三条(基点 1420–1426)按主会话裁决②改判跨系统闭环宿主,不在本宿主断言面内。
|
||||
# 协议照单元侧惯例:extends SceneTree+Kit.check 主通道;--script 模式无 autoload,
|
||||
# ConfigDB 显式实例化构造注入(_fresh_rng 同种子隔离夹具——RngService 构造注入 runner);
|
||||
# 表加载失败不中止(沿被迁断言面的源语义——承 G2–G6 同款)。
|
||||
@@ -579,8 +579,8 @@ func _smoke_feats() -> void:
|
||||
Kit.check("A6 概率面存在(chance=20% 200 展示内必出现——零触发=概率面失守)", offered)
|
||||
a_r.stop()._pending = {"candidates": [{"kind": "relic", "tier": "rare", "feat_id": "x3"}, {"kind": "gp", "tier": "common"}], "payload": {}}
|
||||
Kit.check("relic 兑付:入持有集 + 牌库摘牌(选中踢池——池库存 %d→%d)" % [deck_relic, deck_relic - 1], a_r.stop().pick_curio("relic", "rare", "x3") and (a_r.feats_held() as Array).has("x3") and a_r.deck().pool_stock("relic") == deck_relic - 1)
|
||||
# 断点往返三条(基点 1420–1426:relics 快照/计数器键/余库逐枚还原)按主会话裁决②改判集成宿主——
|
||||
# 驱动代码(save_manager 装配+_on_level_finished+快照取读)随三条同去,集成批落位时重建。
|
||||
# 断点往返三条(基点 1420–1426:relics 快照/计数器键/余库逐枚还原)按主会话裁决②改判跨系统闭环宿主——
|
||||
# 驱动代码(save_manager 装配+_on_level_finished+快照取读)随三条同去,跨系统闭环套件落位时重建。
|
||||
_db.crossrun.feats = live_rows # 还原现行表(共享 _db 防污染——后续冒烟段零感知)
|
||||
# ⑤ V3 订阅面逐件(附表 A 三列——命中面=EffectEngine dispatch 直测;账本面=counter 直读;
|
||||
# 落地行为=发射点施加面〔T1 施加段承载——本段按声明逐件钉命中与边界/反例〕)
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
# ConfigDB 显式实例化构造注入(v2 断点续玩受理与收束链夹具消费 _db);表加载失败不中止
|
||||
# (沿被迁断言面的源语义——诊断在此展开,红由具体断言落,承 G2–G5 同款)。
|
||||
# 测试文件统一 user://smoke_save_*.json 隔离,段首清场+段尾清理——绝不碰 user://save.json(红线)。
|
||||
# 分宿主注记:源 `_smoke_save` ⑨ 摘要集成块(level_autoplay 真局驱动×落盘,基点 :6807–6831)
|
||||
# 经主会话裁决三改判集成测试——不随本宿主,见迁移账本转办清单。
|
||||
# 分宿主注记:源 `_smoke_save` ⑨ 摘要跨系统块(level_autoplay 真局驱动×落盘,基点 :6807–6831)
|
||||
# 经主会话裁决三改判跨系统闭环套件——不随本宿主,见迁移账本转办清单。
|
||||
# 运行:godot --headless --path game --script res://tests/unit/core/save_manager_domain_test.gd
|
||||
extends SceneTree
|
||||
|
||||
@@ -44,7 +44,7 @@ func _fresh_rng(seed_val: int):
|
||||
|
||||
|
||||
# ---- 存档序列化冒烟(SaveManager——读写往返/新档合法/余额拒花/锻造出域拒/高版本拒读禁写/
|
||||
# 损坏兜底/迁移链/域界干净/摘要 cap/摘要集成/生涯终身计数域 v6)。测试文件统一 user://smoke_save_*.json 隔离,
|
||||
# 损坏兜底/迁移链/域界干净/摘要 cap/摘要跨系统块/生涯终身计数域 v6)。测试文件统一 user://smoke_save_*.json 隔离,
|
||||
# 段首清场+段尾清理——绝不碰 user://save.json(红线)。
|
||||
func _smoke_save() -> void:
|
||||
# oracle: design/gdd/systems/表现与支撑/存档序列化.md:176 (存档域读写往返/迁移链/损坏兜底/域界组)
|
||||
@@ -176,8 +176,8 @@ func _smoke_save() -> void:
|
||||
Kit.check("最新在前(头 seed 11 / 尾 seed 2)",
|
||||
int(arr[0].get("master_seed", -1)) == 11 and int(arr[9].get("master_seed", -1)) == 2)
|
||||
|
||||
# ⑨ 摘要集成块(level_autoplay 真局驱动×落盘,基点 :6807–6831)——经主会话裁决三
|
||||
# 改判集成测试,不随本宿主(迁移账本转办清单)。
|
||||
# ⑨ 摘要跨系统块(level_autoplay 真局驱动×落盘,基点 :6807–6831)——经主会话裁决三
|
||||
# 改判跨系统闭环套件,不随本宿主(迁移账本转办清单)。
|
||||
|
||||
# ⑪ 断点域:七字段写读往返逐位一致+附随件+值拷贝出户+清除+旧档迁移合法空态
|
||||
var fbp = sm_script.new(paths[9])
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
# tests/unit/cross_system_flow_test.gd — 跨系统真实路径闭环套件:跨系统真实流×落盘(批 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/unit/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:
|
||||
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
|
||||
_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:
|
||||
# oracle: design/engineering/architecture/architecture.md:90 (事件日志落盘会话组)
|
||||
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:
|
||||
# oracle: design/gdd/systems/表现与支撑/存档序列化.md:180 (专长断点附随件往返组)
|
||||
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:
|
||||
# oracle: design/gdd/systems/跨局循环/元进度.md:150 (摘要跨系统块与钥石只增不减组)
|
||||
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:
|
||||
# oracle: design/engineering/code-conventions.md:40 (试玩日志器跨系统接线组)
|
||||
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()
|
||||
logger.ap_per_round = 8 # 必填注入(R5 守卫——夹具沿组合根表值 8)
|
||||
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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://d0j4m26d1d5yo
|
||||
@@ -20,7 +20,7 @@ func _init() -> void:
|
||||
|
||||
# ---- 启动初始化断言面(宿主接线注记:源函数名 `_initialize` 是 MainLoop 虚拟入口,
|
||||
# 单元宿主改由 `_init` 显式调用,断言逐字未动;源 :70 事件日志落盘条经主会话裁决一
|
||||
# 改判集成测试——不随本宿主,见迁移账本转办清单)----
|
||||
# 改判跨系统闭环套件——不随本宿主,见迁移账本转办清单)----
|
||||
func _smoke_initialize() -> void:
|
||||
# oracle: design/engineering/architecture/architecture.md:90 RngService autoload:open_stream 有状态门面与启动初始化(启动流组)
|
||||
print("=== Dungeon Sweeper 垂直切片冒烟 ===")
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
# tests/unit/node_map_determinism_test.gd — 跨系统真实路径闭环套件:关卡节点图生成·确定性承载
|
||||
# 断言面 = 生成图确定性套(批 B 断言迁移自 smoke_all.gd,G12 施工组;
|
||||
# 主会话裁决六整套不拆——跨四系统且走落盘)。
|
||||
# 跨系统面 = RngService(纯派生流零计数器占用)×
|
||||
# LevelRunner(start_level/start_level_external 受理·生成图装配·单一图源)×
|
||||
# AdventureRunner(开局种子承载·断点写读后同图)×
|
||||
# SaveManager+MetaProgression(首次冒险布尔位收束置位落盘——真实档写盘)。
|
||||
# 与单元侧同入口同协议:extends SceneTree+Kit.check 主通道;--script 模式无 autoload,
|
||||
# ConfigDB 显式实例化构造注入;表加载失败不中止(沿被迁断言面的源语义——诊断展开,红由具体断言落)。
|
||||
# 运行:godot --headless --path game --script res://tests/unit/node_map_determinism_test.gd
|
||||
extends SceneTree
|
||||
|
||||
const Kit := preload("res://tests/unit/test_kit.gd")
|
||||
const SaveManagerScript := preload("res://core/save_manager.gd")
|
||||
const AdventureRunnerScript := preload("res://core/adventure_runner.gd")
|
||||
|
||||
var _db
|
||||
var _bp_path := "user://smoke_node_map_save.json"
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
|
||||
_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_determinism_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_generated_map_determinism()
|
||||
quit(Kit.finish("node_map_determinism_test"))
|
||||
|
||||
|
||||
# ---- 关卡节点图生成·确定性承载断言面 ----
|
||||
func _smoke_generated_map_determinism() -> void:
|
||||
# oracle: design/gdd/systems/冒险循环/关卡节点图生成.md:134 (起关/选路/断点重生成同图组)
|
||||
# ① 纯函数复现:图 = f(参数档, 种子)——同档同种子两次产图逐位同图,换种子必变图
|
||||
var gen = NodeMapGenerator.new(_fresh_rng(910001))
|
||||
var params2: Dictionary = _db.run.level_gen_params.row_by_level_index(2)
|
||||
var g_a: Dictionary = gen.generate(params2, 910002)
|
||||
var g_b: Dictionary = gen.generate(params2, 910002)
|
||||
var g_c: Dictionary = gen.generate(params2, 910003)
|
||||
var nodes_a: Array = g_a.get("nodes", [])
|
||||
Kit.check("同参数档同种子两次产图逐位同图(D2 执行边界)〔v7〕",
|
||||
nodes_a == (g_b.get("nodes", []) as Array))
|
||||
Kit.check("同参数档换种子即变图(图确为种子函数,非恒定输出)",
|
||||
nodes_a != (g_c.get("nodes", []) as Array))
|
||||
Kit.check("产图节点携图内唯一句柄 id 与自入口 depth(消费侧寻址面)",
|
||||
_not_empty_graph(nodes_a) and _handles_unique(nodes_a))
|
||||
# ② 消费侧单一图源:起关装配的图与生成器直取逐位一致(runner 内不存第二份图数据)
|
||||
var runner1 = load("res://core/level_runner.gd").new(_db, _fresh_rng(910004))
|
||||
Kit.check("单关起关受理(种子 910004——生成图直供,无兜底图)",
|
||||
runner1.start_level(2, 910004))
|
||||
Kit.check("起关生效图 = 生成器同档同种子直取逐位一致(消费侧无第二图源)〔v7〕",
|
||||
runner1.level_row.get("nodes", []) == (gen.generate(params2, 910004) as Dictionary).get("nodes", []))
|
||||
# ③ 产图零统一流消费(纯派生流不占计数器号位——断点在任意号位重生成同图的机制承载)
|
||||
var rng_c = _fresh_rng(910005)
|
||||
var counter_before: int = rng_c.counter()
|
||||
var runner2 = load("res://core/level_runner.gd").new(_db, rng_c)
|
||||
runner2.hero_hp = 20 # 冒险模式生命经主角账预置(旧 ledger 注入参数的等效构造面)
|
||||
Kit.check("冒险模式起关受理(入口停点)", runner2.start_level_external(3,
|
||||
Callable(self, "_smoke_rest_stub"), true))
|
||||
Kit.check("起关产图零计数器消费(同图不依赖取数号位)", rng_c.counter() == counter_before)
|
||||
var params3: Dictionary = _db.run.level_gen_params.row_by_level_index(3)
|
||||
var entry_count: int = int(params3.get("entry_count", -1))
|
||||
Kit.check("入口停点候选数=参数档入口个数(L3 档 %d——多入口首跳即选路,禁写死单入口)〔v2〕" % entry_count,
|
||||
entry_count >= 1 and (runner2.next_candidates() as Array).size() == entry_count)
|
||||
if entry_count >= 2:
|
||||
var eids: Array = runner2.next_candidate_ids()
|
||||
Kit.check("第二入口经句柄显式进入(句柄 %d)〔v2〕" % int(eids[1]),
|
||||
runner2.pick_next_node(int(eids[1])) and int(runner2.current_node_id()) == int(eids[1]))
|
||||
# ④ 开局种子承载:新档(首局)种子 42 两次开局逐位同图,且与生成器直取一致
|
||||
DirAccess.remove_absolute(_bp_path) # 测试隔离——上轮落盘残留先删,断点面从零档起
|
||||
var sv_w = SaveManagerScript.new(_bp_path)
|
||||
var adr_x = AdventureRunnerScript.new(_db, _fresh_rng(42))
|
||||
adr_x.set_save_manager(sv_w) # 先挂档再开局——停点快照由真实写手落盘
|
||||
Kit.check("首局口径种子 42 开局受理(defer_entry 路径)", adr_x.start_adventure(42,
|
||||
{"hero_override": {}, "treasure_unlocked": {}}))
|
||||
var adr_y = AdventureRunnerScript.new(_db, _fresh_rng(42))
|
||||
Kit.check("同种子第二次开局受理", adr_y.start_adventure(42,
|
||||
{"hero_override": {}, "treasure_unlocked": {}}))
|
||||
Kit.check("种子 42 两次开局图逐位同图(教学局恒同图——首局固定种子的确定性承载)〔v7〕",
|
||||
adr_x.runner().nodes() == adr_y.runner().nodes())
|
||||
Kit.check("开局图 = L1 参数档在种子 42 下的生成图(开局无隐式取数分叉)",
|
||||
adr_x.runner().nodes() == (gen.generate(_db.run.level_gen_params.row_by_level_index(1), 42) as Dictionary).get("nodes", []))
|
||||
# ⑤ 断点写→恢复→再产图同图(续玩按主种子+参数档重生成,断点不存图快照)
|
||||
Kit.check("停点断点落盘在档", sv_w.has_breakpoint())
|
||||
var bp: Dictionary = sv_w.breakpoint_snapshot()
|
||||
var adr_z = AdventureRunnerScript.new(_db, load("res://core/rng_service.gd").new())
|
||||
Kit.check("断点续玩受理(代差标记键在档)〔v8〕", adr_z.resume_adventure(bp,
|
||||
{"hero_override": {}, "treasure_unlocked": {}}))
|
||||
Kit.check("续玩重生成图与中断前逐位同图〔v7〕", adr_z.runner().nodes() == adr_x.runner().nodes())
|
||||
var adr_old = AdventureRunnerScript.new(_db, load("res://core/rng_service.gd").new())
|
||||
var bp_old: Dictionary = bp.duplicate(true)
|
||||
bp_old.erase("graph_gen_v2") # 换代前旧断点形态(模板池时代无代差标记键)
|
||||
Kit.check("缺生成图代差标记键的旧断点被显式拒绝(禁按新语义静默回放)〔v8〕",
|
||||
adr_old.resume_adventure(bp_old, {}) == false)
|
||||
# ⑥ 关卡表不再携带图数据(图源换代后 level 行只剩行级字段)
|
||||
var tpl_free := true
|
||||
for li in range(1, _db.run.level.level_count() + 1):
|
||||
if (_db.run.level.row_by_level_index(li) as Dictionary).has("templates"):
|
||||
tpl_free = false
|
||||
Kit.check("level 表三行均无 templates 键(模板数据已清出)〔v9〕",
|
||||
tpl_free and _db.run.level.level_count() == 3)
|
||||
# ⑦ 收束置位:新档 false → settle 置 true 恰一次(幂等)
|
||||
var sv = SaveManagerScript.new(_bp_path + ".flag.json")
|
||||
sv.load_from_disk()
|
||||
Kit.check("新档首次冒险布尔位 = false(默认域——首局恒走固定种子 42)",
|
||||
sv.meta_state.career_state().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.meta_state.career_state().first_adventure_done == true)
|
||||
mp.settle_adventure({"levels_cleared": 0, "keystone_total": 0, "cleared": false, "result": "failed"})
|
||||
Kit.check("置位幂等(重复收束不翻转)", sv.meta_state.career_state().first_adventure_done == true)
|
||||
DirAccess.remove_absolute(_bp_path)
|
||||
DirAccess.remove_absolute(_bp_path + ".flag.json")
|
||||
|
||||
|
||||
# 图数据良构前置(防空集假绿——节点数 > 1、句柄唯一、depth 与 next 键在位)
|
||||
func _not_empty_graph(nodes: Array) -> bool:
|
||||
if nodes.size() < 2:
|
||||
return false
|
||||
for node in nodes:
|
||||
var n := node as Dictionary
|
||||
if not n.has("id") or not n.has("depth") or not n.has("next"):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _handles_unique(nodes: Array) -> bool:
|
||||
var seen := {}
|
||||
for node in nodes:
|
||||
var id: int = int((node as Dictionary).get("id", -1))
|
||||
if id < 0 or seen.has(id):
|
||||
return false
|
||||
seen[id] = true
|
||||
return true
|
||||
|
||||
|
||||
func _fresh_rng(seed_val: int):
|
||||
var r = load("res://core/rng_service.gd").new()
|
||||
r.new_adventure(seed_val)
|
||||
return r
|
||||
|
||||
|
||||
# ---- 恢复执行体(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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dhhxar186w1sq
|
||||
@@ -15,7 +15,7 @@
|
||||
# ConfigDB/RngService 显式实例化构造注入(统一流 master 种子 42 沿源);表加载失败不中止
|
||||
# (沿被迁断言面的源语义——诊断在此展开,红由具体断言落,承 G2–G5 同款);全局 class_name
|
||||
# (UiKit)裸用(--script 模式全局类在册,G3 先例)。
|
||||
# 分宿主注记:源 `_smoke_playtest_logger`(挂真实结算流跨系统)经主会话裁决七改判集成测试,
|
||||
# 分宿主注记:源 `_smoke_playtest_logger`(挂真实结算流跨系统)经主会话裁决七改判跨系统闭环套件,
|
||||
# 不随本宿主——见迁移账本转办清单;`_smoke_global_theme` 依赖 ThemeDB 项目主题在座,
|
||||
# 裁决四要求实跑验证(本批零测试运行,验证归装配验证轮)。
|
||||
# 运行:godot --headless --path game --script res://tests/unit/presentation/theme_projection_test.gd
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
# tests/unit/save_rng_roundtrip_test.gd — 跨系统真实路径闭环套件:存档 ⇄ RNG 快照往返
|
||||
# 跨系统面 = SaveManager(落盘 + 版本迁移链)× AdventureRunner(断点消费)×
|
||||
# RngService(种子/取数号位续跑)× BoardGenerator(开板确定性)。
|
||||
# 与 tests/unit/core/breakpoint_resume_test.gd 的分工:那件是单测,只验「断点写读 + 首战开板一致」;
|
||||
# 本件验的是**跨系统闭环**——盘上往返(含旧版本档经迁移链)之后,断点仍能被冒险层消费且 RNG 续跑
|
||||
# 零跳号,是「玩家的旧存档还能不能接着打」这条真实路径。
|
||||
# 运行:godot --headless --path game --script res://tests/unit/save_rng_roundtrip_test.gd
|
||||
extends SceneTree
|
||||
|
||||
const SaveManagerScript := preload("res://core/save_manager.gd")
|
||||
const AdventureRunnerScript := preload("res://core/adventure_runner.gd")
|
||||
const RngServiceScript := preload("res://core/rng_service.gd")
|
||||
const Kit := preload("res://tests/unit/test_kit.gd")
|
||||
|
||||
const SEED := 4242
|
||||
const SAVE_PATH := "user://test_integration_save_rng.json"
|
||||
|
||||
var _db: ConfigDb
|
||||
var _disk: String
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
|
||||
_disk = ProjectSettings.globalize_path(SAVE_PATH)
|
||||
_clear_disk()
|
||||
_db = load("res://core/config_db.gd").new() as ConfigDb
|
||||
if not _db.load_tables("res://data/tables", false):
|
||||
Kit.check("数值表加载", false)
|
||||
quit(Kit.finish("save_rng_roundtrip"))
|
||||
return
|
||||
_test_disk_roundtrip_preserves_breakpoint()
|
||||
_test_old_version_save_migrates_and_resumes()
|
||||
_clear_disk()
|
||||
quit(Kit.finish("save_rng_roundtrip"))
|
||||
|
||||
|
||||
# ---- 用例一:盘上往返保真——写档再读回,断点逐键零漂移 ----
|
||||
func _test_disk_roundtrip_preserves_breakpoint() -> void:
|
||||
# oracle: design/gdd/systems/表现与支撑/存档序列化.md:180 (存档⇄RNG 快照往返——断点域逐位组)
|
||||
var rng := RngServiceScript.new()
|
||||
var sm := SaveManagerScript.new(SAVE_PATH)
|
||||
var adr := AdventureRunnerScript.new(_db, rng)
|
||||
adr.set_save_manager(sm)
|
||||
Kit.check_quiet(adr.start_adventure(SEED, {"hero_override": {}, "treasure_unlocked": {}}),
|
||||
"往返用例:冒险启动")
|
||||
Kit.check_quiet(sm.has_breakpoint(), "往返用例:入口停点断点在档")
|
||||
Kit.check_quiet(sm.write_to_disk(), "往返用例:断点写盘")
|
||||
|
||||
var before: Dictionary = sm.breakpoint_snapshot()
|
||||
var sm2 := SaveManagerScript.new(SAVE_PATH)
|
||||
Kit.check_quiet(sm2.load_from_disk(), "往返用例:自盘读回受理")
|
||||
Kit.check_quiet(sm2.has_breakpoint(), "往返用例:读回后断点仍在档")
|
||||
var after: Dictionary = sm2.breakpoint_snapshot()
|
||||
Kit.check_quiet(int(after.get("master_seed", 0)) == SEED, "往返用例:主种子往返保真")
|
||||
Kit.check_quiet(int(after.get("rng_counter", -1)) == int(before.get("rng_counter", -2)),
|
||||
"往返用例:RNG 取数号位往返保真(存档不得重耗/跳号)")
|
||||
var bonus_before: Dictionary = before.get("bonus", {}) as Dictionary
|
||||
var bonus_after: Dictionary = after.get("bonus", {}) as Dictionary
|
||||
var bonus_same := bonus_after.size() == bonus_before.size()
|
||||
for k in bonus_before:
|
||||
if int(bonus_after.get(k, -99999)) != int(bonus_before[k]):
|
||||
bonus_same = false
|
||||
Kit.check_quiet(bonus_same, "往返用例:永久强化账本往返保真(逐键比整数值——JSON 往返不改值)")
|
||||
Kit.check_quiet(int(after.get("hero_hp", -1)) == int(before.get("hero_hp", -2)),
|
||||
"往返用例:生命账本往返保真")
|
||||
|
||||
|
||||
# ---- 用例二:旧版本档经迁移链后仍可续玩(单测不覆盖的跨系统闭环面)----
|
||||
func _test_old_version_save_migrates_and_resumes() -> void:
|
||||
# oracle: design/gdd/systems/表现与支撑/存档序列化.md:179 (旧档迁移链读回续玩组)
|
||||
var rng := RngServiceScript.new()
|
||||
var sm := SaveManagerScript.new(SAVE_PATH)
|
||||
var adr := AdventureRunnerScript.new(_db, rng)
|
||||
adr.set_save_manager(sm)
|
||||
if not adr.start_adventure(SEED, {"hero_override": {}, "treasure_unlocked": {}}):
|
||||
Kit.check("旧档用例:冒险启动前置", false)
|
||||
return
|
||||
var runner = adr.runner()
|
||||
var c0: int = rng.counter()
|
||||
var cand: Array = runner.next_candidate_ids()
|
||||
if cand.is_empty():
|
||||
Kit.check("旧档用例:入口停点有候选路", false)
|
||||
return
|
||||
var fp_before := _board_fingerprint(runner)
|
||||
var hp_before: int = runner.hero_hp
|
||||
sm.write_to_disk()
|
||||
|
||||
# 降级为上一版格式档:抹掉当前版本新增域并回退版本号,模拟玩家手里的旧档。
|
||||
var raw := FileAccess.get_file_as_string(_disk)
|
||||
var parsed: Variant = JSON.parse_string(raw)
|
||||
if not (parsed is Dictionary):
|
||||
Kit.check("旧档用例:盘上档可解析", false)
|
||||
return
|
||||
var old: Dictionary = (parsed as Dictionary).duplicate(true)
|
||||
old["version"] = 6
|
||||
var meta: Dictionary = old.get("meta", {}) as Dictionary
|
||||
meta.erase("forge_routes")
|
||||
old["meta"] = meta
|
||||
var f := FileAccess.open(_disk, FileAccess.WRITE)
|
||||
if f == null:
|
||||
Kit.check("旧档用例:旧档可落盘", false)
|
||||
return
|
||||
f.store_string(JSON.stringify(old))
|
||||
f.close()
|
||||
|
||||
var sm_old := SaveManagerScript.new(SAVE_PATH)
|
||||
Kit.check_quiet(sm_old.load_from_disk(), "旧档用例:旧档经迁移链可载")
|
||||
Kit.check_quiet(sm_old.has_breakpoint(), "旧档用例:迁移后断点未被迁移链吃掉")
|
||||
var forge: Dictionary = sm_old.meta_state.forge_state()
|
||||
Kit.check_quiet(str(forge.get("weapon", "")) == "sword",
|
||||
"旧档用例:迁移补齐当前版本新增域(缺 forge_routes 的旧档按初始持有态可玩)")
|
||||
|
||||
# 旧档断点仍能被冒险层消费,且 RNG 续跑零跳号、开板与中断前逐位一致
|
||||
var bp: Dictionary = sm_old.breakpoint_snapshot()
|
||||
var rng2 := RngServiceScript.new()
|
||||
var adr2 := AdventureRunnerScript.new(_db, rng2)
|
||||
Kit.check_quiet(adr2.resume_adventure(bp, {"hero_override": {}, "treasure_unlocked": {}}),
|
||||
"旧档用例:旧档断点续玩受理")
|
||||
Kit.check_quiet(int(adr2.master_seed) == SEED, "旧档用例:续玩主种子回填")
|
||||
Kit.check_quiet(rng2.counter() == c0, "旧档用例:续玩取数号位 = 断点号位(零重耗零跳号)")
|
||||
var runner2 = adr2.runner()
|
||||
Kit.check_quiet(runner2.hero_hp == hp_before, "旧档用例:续玩生命账本回填")
|
||||
Kit.check_quiet(_board_fingerprint(runner2) == fp_before,
|
||||
"旧档用例:续玩停点态与中断前逐位一致")
|
||||
|
||||
|
||||
func _board_fingerprint(runner) -> String:
|
||||
# 停点态指纹:候选路 + 生命 + 取数号位(无战斗对象时的可比面)
|
||||
var cand: Array = runner.next_candidate_ids()
|
||||
return "%s|%d" % [str(cand), runner.hero_hp]
|
||||
|
||||
|
||||
func _clear_disk() -> void:
|
||||
if FileAccess.file_exists(_disk):
|
||||
DirAccess.remove_absolute(_disk)
|
||||
@@ -0,0 +1 @@
|
||||
uid://c3bnu5cpcgkil
|
||||
Reference in new issue
Block a user