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

+3 -1
View File
@@ -258,7 +258,9 @@ static func _on_adventure_wallet(event: String, gp_val: int, extra: Dictionary,
# 三选一停点驱动(确定性策略;TP0 起 316-f 判读:1 级最稀有、同档并列取最左;
# 奖励事件随 reward_events 留档)
static func _on_adventure_bonus(payload: Dictionary, st: Dictionary, adr) -> void:
st.reward_events.append({"curio_pick": payload.duplicate(true)})
var pl := payload.duplicate(true)
pl["bonus_max_hp"] = int(adr.run_ledger.bonus.get("max_hp", 0)) # 观测面:停点时刻加算区快照(回血精确断言用)
st.reward_events.append({"curio_pick": pl})
var candidates: Array = payload.get("candidates", []) as Array
if candidates.is_empty():
st.result = "advance_failed"
File diff suppressed because it is too large. Load diff
-1
View File
@@ -1 +0,0 @@
uid://bk7yf7sf5w3h3
@@ -1,74 +0,0 @@
# tests/unit/battle_settlement/seal_polarity_test.gd — 封印与极性单元测试宿主
# 断言面 = 八套(批 B 断言迁移自 smoke_all.gd,G5 施工组;八套按源编排序分八笔原子提交迁入):
# 末点 AP 封印轮末结算(playtest 缺陷②回归——action_economy 末点花费与轮终结)
# 封印不可逆四情况(禁撤封印 + 挖封印格兑付改判——minesweeper_rules 拒绝面)
# 清图奖励双份兑付链(真实流完美封印——orchestrator grant_clear 身份兑付)
# 清图奖励弱形式分支(零兑付 / 高光守卫 / 第二份拒——直调编排私有面构造)
# 轮内刷新 provenance 守卫(刷新图清图不发清图奖励)
# 极性路由 + 封印溢价 125%(挖安全格正向 / 挖危险格负向 / 标中标空兑付极性)
# 负向字典逐型直验(效果行 / heal 直伤 / blast 警觉 / action 行动点排队)
# 格面资源资格面(揭示事件 resource 标记 + 封锁格身份查询)
# 协议照单元侧惯例:extends SceneTree+Kit.check 主通道;--script 模式无 autoload,
# ConfigDB 显式实例化构造注入(各套自建 RngService——源套掷点隔离形态,宿主不持统一流);
# 表加载失败不中止(沿被迁断言面的源语义——诊断在此展开,表值变异的红由具体断言落,承 G2/G3/G4 同款)。
# 运行:godot --headless --path game --script res://tests/unit/battle_settlement/seal_polarity_test.gd
extends SceneTree
const Kit := preload("res://tests/unit/test_kit.gd")
const ConfigDBScript := preload("res://core/config_db.gd")
const AutoPlayer := preload("res://smoke/auto_player.gd") # 自动玩家策略单一实现(迁移期按现路径 preload——账本 §五夹具前提)
var _db
func _init() -> void:
_db = ConfigDBScript.new()
# 表加载失败不中止(承 G2/G3/G4 同款:表损坏的红落在具体断言上——
# 诊断在此展开,后续断言按坏表实况逐条验红;collect_only=true 下成员照常赋值可评)
var load_ok: bool = _db.load_tables("res://data/tables", true)
if not load_ok:
push_error("[seal_polarity_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_seal_ap_exhausted()
quit(Kit.finish("seal_polarity_test"))
# ---- 末点 AP 封印轮末结算(playtest 缺陷②回归:末点 AP 花在封印必须终结轮次进入下一轮)----
func _smoke_seal_ap_exhausted() -> void:
print("--- 排雷玩法·封印动作耗尽末点 AP 回归 ---")
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(20260911)
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
var rounds: Array = []
settlement.round_started.connect(func(ri): rounds.append(ri))
Kit.check("回归战斗启动(Boss 行——D0 20HP 级会被盲挖粉尘秒杀,无法承载封印回归场景)",
settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}))
var minesweeper = settlement.minesweeper
# 恰花 (AP-1) 点挖掘,留末点给封印(表驱动——AP 从表读不写死)
var ap_total: int = int(_db.action_economy.ap_per_round)
for i in range(ap_total - 1):
if settlement.action_economy.phase != "Active" or settlement.action_economy.ap == 0:
break
var plan = AutoPlayer.choose_dig(minesweeper)
if plan.is_empty():
break
if plan.hero_cell != minesweeper.hero_pos():
minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}])
minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
Kit.check("%d 次挖掘后恰剩 1 AP(phase=%s ap=%d)" % [ap_total - 1, str(settlement.action_economy.phase), settlement.action_economy.ap],
settlement.action_economy.phase == "Active" and settlement.action_economy.ap == 1)
# 末点 AP 花在封印——轮次必须终结并结算进入第 2 轮
# 封印目标 = 挖掘策略的下一目标格(构造保证:未开、与小人邻接——先把小人移到其邻接已开格)
var plan2 = AutoPlayer.choose_dig(minesweeper)
Kit.check("仍存在可挖目标(封印目标来源)", not plan2.is_empty())
var out: Array = []
if not plan2.is_empty():
if plan2.hero_cell != minesweeper.hero_pos():
minesweeper.settle_batch([{"action": "move", "cell": plan2.hero_cell}])
out = minesweeper.settle_batch([{"action": "seal_place", "cell": plan2.dig_cell}])
Kit.check("末点 AP 封印被受理", not out.is_empty() and bool(out[0].accepted))
Kit.check("封印动作耗尽 AP 触发轮末结算并进入第 2 轮(rounds=%s)" % str(rounds), rounds.size() >= 2)
@@ -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)
File diff suppressed because it is too large. Load diff
-159
View File
@@ -1,159 +0,0 @@
# tests/mutation/mutation_plan.gd — 变异验证引擎的计划配置(数据件,无逻辑)
# 宿主×套×变异策略一览:套→守的生产模块(账本 §二末列)+武器优先序+运行参数。
# src 指「被变换编译的宿主源码」:稳定宿主直接读工作区文件;迁移中的宿主(如 seal)
# 用驱动命令物化的快照(user:// 下,仓库外)——物化命令见套配置旁注与台账头部。
# 武器代号:table=表/资源改写(内存字段写读还原)· inject=注入编译(生产源码内存变换
# reload 原地替换·零触盘)· state=状态篡改(静态变量/成员初值/autoload 实例与树状态)。
# budget=每套改坏点试跑上限(轮)· cap_ms=每套墙钟上限 · count=静态断言点数(对账用)。
extends RefCounted
const GLOBAL_WATCHDOG_MS := 14400000 # 引擎全局看门狗(防挂死占闸;外层 bash 超时为最终兜底——4h,慢套全程靠它)
const SUITE_CAP_MS_DEFAULT := 600000 # 单套墙钟上限缺省(宿主级 cap_ms 可覆盖)
const BUDGET_DEFAULT := 160 # 单套试跑轮数缺省
static func hosts() -> Array:
return [
{
"id": "system_test",
"src": "res://tests/system_test.gd",
"channel": "self", # 断言通道 = 宿主自有 check(cond, label)
"async": true, # 套函数为协程(await process_frame)
"budget": 240,
"cap_ms": 2400000, # 慢套墙钟上限(boot 链/活战斗驱动单轮 20–60s)
"state_autoloads": ["ConfigDB", "SaveManager"],
"suites": {
"_split_tail": {"count": 1, "weapons": ["state"], "modules": [], "tables": [],
"strip_siblings": true, "funcs": {}},
"_smoke_scene_split": {"count": 11, "weapons": ["inject", "state"], "tables": [],
"modules": ["res://presentation/scenes/menu_screen.gd", "res://presentation/scenes/camp_screen.gd",
"res://presentation/scenes/adventure_screen.gd", "res://presentation/main.gd"],
"funcs": {"menu_screen.gd": ["_ready", "_build_ui"], "camp_screen.gd": ["_ready", "_build_ui"],
"adventure_screen.gd": ["_ready", "_build_ui"], "main.gd": ["_apply_layer_contract", "_build_ui", "_ready"]}},
"_smoke_boot_chain": {"count": 15, "weapons": ["inject", "state"], "tables": [],
"modules": ["res://presentation/settings_store.gd", "res://core/save_manager.gd",
"res://presentation/main.gd", "res://presentation/views/ui_kit.gd", "res://core/config_db.gd"],
"funcs": {"settings_store.gd": ["*"], "save_manager.gd": ["active_slot", "keystone_balance",
"write_to_disk", "has_breakpoint", "set_breakpoint", "clear_breakpoint", "slot_path", "adventure_total", "has_write_target"],
"main.gd": ["_ready", "_build_ui", "_refresh_resume_button", "_begin_session"]}},
"_smoke_run_lifecycle": {"count": 11, "weapons": ["inject", "state"], "tables": [],
"modules": ["res://presentation/run_scope.gd", "res://presentation/views/battle_end_overlay.gd",
"res://presentation/main.gd", "res://core/adventure_runner.gd"],
"funcs": {"run_scope.gd": ["*"], "battle_end_overlay.gd": ["is_open", "open", "close"],
"main.gd": ["_start_adventure_from_menu", "_begin_session", "_on_battle_end_continue", "_refresh_resume_button"],
"adventure_runner.gd": ["abandon", "clear_breakpoint"]}},
"_smoke_lifecycle_matrix": {"count": 10, "weapons": ["inject", "state"], "tables": [],
"modules": ["res://presentation/scenes/menu_screen.gd", "res://core/battle_orchestrator.gd",
"res://presentation/views/battle_end_overlay.gd", "res://presentation/main.gd", "res://core/adventure_runner.gd"],
"funcs": {"menu_screen.gd": ["_build_ui"], "battle_orchestrator.gd": ["is_battle_live", "begin_battle"],
"battle_end_overlay.gd": ["is_open"], "main.gd": ["_begin_session", "_on_battle_end_continue",
"_resume_from_menu", "_open_settings", "_open_career", "_show_debug_overlay"],
"adventure_runner.gd": ["abandon", "clear_breakpoint"]}},
"_stage_faces": {"count": 1, "run_fn": "_smoke_lifecycle_matrix", "weapons": ["inject", "state"],
"tables": [], "modules": ["res://presentation/modal_flow.gd", "res://presentation/main.gd"],
"funcs": {"modal_flow.gd": ["*"], "main.gd": ["_setup_modal_flow", "_build_ui"]}},
},
},
{
"id": "table_structure_guard",
"src": "res://tests/unit/data_config/table_structure_guard_test.gd",
"channel": "kit",
"async": false,
"budget": 3000,
"cap_ms": 1200000,
"state_autoloads": [],
"suites": {
"_smoke_ac_swap_guard": {"count": 12, "weapons": ["table", "inject"],
"tables": ["enemy", "status_effects", "reward", "settlement", "hero"],
"modules": ["res://core/unit_container.gd", "res://core/config_db_resolver.gd"], "funcs": {}},
"_smoke_enemy_bands": {"count": 4, "weapons": ["table", "inject"],
"tables": ["enemy"], "modules": ["res://core/config_db_resolver.gd"], "funcs": {}},
"_smoke_variation_framework": {"count": 9, "weapons": ["table", "inject"],
"tables": ["variation"], "modules": ["res://data/schemas/variation_table.gd"], "funcs": {}},
"_smoke_difficulty_table": {"count": 8, "weapons": ["table", "inject"],
"tables": ["difficulty"], "modules": ["res://core/unit_container.gd", "res://core/config_db_resolver.gd"], "funcs": {}},
"_smoke_nine_types_gp_stream": {"count": 6, "weapons": ["table", "inject"],
"tables": ["reward"], "modules": ["res://data/schemas/reward_table.gd", "res://core/rng_service.gd"], "funcs": {}},
"_smoke_adventure_table": {"count": 17, "weapons": ["table", "inject"],
"tables": ["adventure", "treasure_items", "meta"],
"modules": ["res://data/schemas/adventure_table.gd", "res://core/attunement.gd",
"res://data/schemas/treasure_item_table.gd", "res://core/battle_orchestrator.gd", "res://core/meta_progression.gd",
"res://core/config_db.gd"], "funcs": {}},
"_smoke_meta": {"count": 4, "weapons": ["table", "inject"],
"tables": ["meta"], "modules": ["res://core/meta_progression.gd"], "funcs": {}},
},
},
{
"id": "board_gen_reward",
"src": "res://tests/unit/core/board_gen_reward_test.gd",
"channel": "kit",
"async": false,
"budget": 800,
"state_autoloads": [],
"suites": {
"_smoke_layout_cell_formula": {"count": 2, "weapons": ["inject", "state"], "tables": [],
"modules": ["res://presentation/views/board_view.gd"], "funcs": {}},
"_smoke_reward_identity_unit": {"count": 10, "weapons": ["inject", "state"], "tables": [],
"modules": ["res://core/reward_economy.gd"], "funcs": {}},
"_smoke_board_generator": {"count": 22, "weapons": ["inject", "table"],
"tables": ["board_gen"],
"modules": ["res://core/board_generator.gd", "res://core/config_db_resolver.gd"], "funcs": {}},
"_smoke_power_interval": {"count": 5, "weapons": ["table", "inject"], "tables": ["reward"],
"modules": ["res://core/reward_economy.gd"], "funcs": {}},
},
},
{
"id": "roll_multiplier",
"src": "res://tests/unit/battle_settlement/roll_multiplier_test.gd",
"channel": "kit",
"async": false,
"budget": 400,
"cap_ms": 1800000,
"state_autoloads": [],
"suites": {
"_smoke_enemy_attack_type": {"count": 4, "weapons": ["table", "inject"],
"tables": ["enemy"], "modules": ["res://core/unit_container.gd", "res://core/damage_controller.gd"], "funcs": {}},
"_smoke_armor_roll": {"count": 15, "weapons": ["table", "inject"], "tables": ["settlement"],
"modules": ["res://core/damage_math.gd", "res://core/damage_controller.gd", "res://core/status_container.gd"], "funcs": {}},
"_smoke_pool_wiring": {"count": 4, "weapons": ["table", "inject"], "tables": ["status_effects"],
"modules": ["res://core/damage_controller.gd"], "funcs": {}},
"_smoke_damage_types": {"count": 10, "weapons": ["table", "inject"], "tables": ["enemy", "settlement"],
"modules": ["res://core/settlement/grant_segment.gd", "res://core/damage_controller.gd"], "funcs": {}},
"_smoke_attack_chain_fresh_pool": {"count": 3, "weapons": ["inject", "state"], "tables": [],
"modules": ["res://core/battle_orchestrator.gd", "res://core/damage_controller.gd"], "funcs": {}},
},
},
{
"id": "seal_polarity",
# 迁移中宿主:读驱动命令物化的提交快照(5ed1ec99 版=套1 五条在册形态),不读 WIP 工作区
"src": "user://mutation_work/seal_polarity_test.gd",
"channel": "kit",
"async": false,
"budget": 600,
"state_autoloads": [],
"suites": {
"_smoke_seal_ap_exhausted": {"count": 5, "weapons": ["state", "table", "inject"],
"tables": ["action_economy"],
"modules": ["res://core/battle_orchestrator.gd", "res://core/action_economy.gd", "res://core/minesweeper_rules.gd"],
# funcs 提示=账本 §九 G5 套1 旧法变异点所在函数(只作试跑排序,不作唯一改坏面)
"funcs": {"battle_orchestrator.gd": ["begin_battle"],
"minesweeper_rules.gd": ["revealed_cells", "settle_batch"],
"action_economy.gd": ["_cost", "try_spend", "_spend_cost_and_arbitrate", "_spend_and_arbitrate"]}},
},
},
]
static func host_by_id(id: String) -> Dictionary:
for h in hosts():
if h["id"] == id:
return h
return {}
static func total_static_checks() -> int:
var n := 0
for h in hosts():
for s in h["suites"].values():
n += int(s["count"])
return n
+11
View File
@@ -49,6 +49,7 @@ func _suites() -> Array:
{"path": "res://tests/unit/data_config/counter_prefix_fatal_test.gd", "expect": 1, "negative": true}, # 卡② V9 前缀合法性+引用闭合加载致命
{"path": "res://tests/unit/data_config/rng_service_test.gd", "expect": 0},
{"path": "res://tests/unit/data_config/table_structure_guard_test.gd", "expect": 0}, # 表结构守卫宿主(adventure 配额与同调目录/enemy 带键行序/AC 单轨与掷点参数/variation 门控互锁/difficulty 修饰/reward 九型/meta 行族)
{"path": "res://tests/unit/data_config/rng_stream_boot_test.gd", "expect": 0}, # 启动初始化与统一 RNG 流宿主(data/tables 加载链/统一流黄金向量 m=42/derive_at 恒等/open_stream 取号序/new_adventure 计数器重置——批 B 自 smoke_all 迁入;源 :70 事件日志落盘条按裁决一转办集成宿主)
{"path": "res://tests/unit/core/treasure_items_test.gd", "expect": 0}, # 秘宝结构冻结断言面
{"path": "res://tests/unit/core/breakpoint_resume_test.gd", "expect": 0}, # 断点往返/续玩确定性/幂等/退出清断点
{"path": "res://tests/unit/core/curio_refresh_paid_test.gd", "expect": 0}, # 三选一刷新免费双源+GP 付费档(对齐商店 MC-1:价曲线/优先序/停点清零/钱包单口)
@@ -61,8 +62,11 @@ func _suites() -> Array:
{"path": "res://tests/unit/battle_settlement/damage_controller_test.gd", "expect": 0},
{"path": "res://tests/unit/battle_settlement/roll_multiplier_test.gd", "expect": 0}, # 掷点与乘区宿主(敌攻击类型轴/护甲掷点行为面/乘区接线/伤害类型克制与标错/fresh 池攻击链——批 B 自 smoke_all 迁入)
{"path": "res://tests/unit/battle_settlement/seal_polarity_test.gd", "expect": 0}, # 封印与极性宿主(末点 AP 封印/封印不可逆四情况/清图奖励双份与弱形式/刷新 provenance 守卫/极性路由与封印溢价/负向字典/格面资源资格——批 B 自 smoke_all 迁入)
{"path": "res://tests/unit/battle_settlement/round_flow_skill_pool_test.gd", "expect": 0}, # 轮内流与技能池宿主(全战斗回放/轮内刷新与 V5 快照单源/遭遇区间骰/结算五段全序/技能池数据面·回放独立·EV 锚·死亡打断·校验拒绝/妨害附加/四桶计数——批 B 自 smoke_all 迁入)
{"path": "res://tests/unit/battle_settlement/variant_placement_test.gd", "expect": 0}, # 变异布置宿主(战内布置段门控与数量公式/全局变异兜底回落与资源面偏斜——批 B 自 smoke_all 迁入)
{"path": "res://tests/unit/battle_settlement/skill_unlock_test.gd", "expect": 0}, # Boss 半血解锁技·容器机制(门谓词/闩不可逆/双面过滤/保底不逼锁定/零扰动)
{"path": "res://tests/unit/contracts/system_contracts_test.gd", "expect": 0},
{"path": "res://tests/unit/contracts/source_guard_test.gd", "expect": 0}, # 源码契约宿主(project.godot 警告配置与类型闸/产品码卫生死代码·影子账·层序单口——批 B 自 smoke_all 迁入;被测对象=仓库自身源文件,裁决五新设)
{"path": "res://tests/unit/presentation/ui_font_asset_test.gd", "expect": 0},
{"path": "res://tests/unit/presentation/terrain_tile_sets_test.gd", "expect": 0},
# 提灯载体行不在册(呈现桥断言归阔视野带参数面)
@@ -79,15 +83,22 @@ func _suites() -> Array:
{"path": "res://tests/unit/presentation/modal_flow_test.gd", "expect": 0}, # 模态编排纯逻辑(V6:直接 new/互斥域谓词/三例外锁/演播后排窗)
# ---- L2 集成层(跨系统用例,与 unit/ 的隔离断言面分工)----
{"path": "res://tests/integration/save_rng_roundtrip_test.gd", "expect": 0}, # 存档⇄RNG 快照往返(SaveManager 迁移链 × 断点消费 × RNG 续跑 × 停点态确定性)
{"path": "res://tests/integration/node_map_templates_test.gd", "expect": 0}, # 关卡节点图生成·模板池(模板池跨系统×首冒险置位落盘——批 B 自 smoke_all 迁入,裁决六整套集成不拆)
{"path": "res://tests/integration/cross_system_flow_test.gd", "expect": 0}, # 跨系统真实流×落盘(RNG 事件日志落盘/专长断点往返/存档摘要集成/试玩记录仪真实结算流——批 B 转办块宿主,裁决一/二/三/七)
# ---- core 四件与呈现层两件的直接单测 ----
{"path": "res://tests/unit/core/board_generator_test.gd", "expect": 0}, # 棋盘生成(尺寸合表/数字=八邻雷数/同种子逐位一致/可推理自洽)
{"path": "res://tests/unit/core/board_gen_reward_test.gd", "expect": 0}, # 棋盘生成与地图战斗资源宿主(格边长公式/资源身份面/生成器三分出口与 r0 闸/伤害量值区间——批 B 自 smoke_all 迁入)
{"path": "res://tests/unit/core/adventure_feats_test.gd", "expect": 0}, # 冒险结构与专长宿主(状态机受理面/三关序列与账本封闭/败北线/feat_table 四级/专长凭证保底/V3 订阅面逐件——批 B 自 smoke_all 迁入;断点往返三条按裁决二转办集成宿主)
{"path": "res://tests/unit/core/level_shop_economy_test.gd", "expect": 0}, # 关卡图与 GP 商店宿主(level 图校验与推进/enemy 行集定点/钱包到账/escrow 钳制/商店三收口/共享池抽买——批 B 自 smoke_all 迁入)
{"path": "res://tests/unit/core/meta_progression_forge_test.gd", "expect": 0}, # 元进度与锻造宿主(锻造两轴/树购买受理面/hero_override 注入/收束入账与工坊解锁/树六键合成——批 B 自 smoke_all 迁入)
{"path": "res://tests/unit/core/save_manager_domain_test.gd", "expect": 0}, # 存档域宿主(读写往返/迁移链/损坏兜底/域界干净/摘要 cap/断点域七字段/三槽位承载——批 B 自 smoke_all 迁入;摘要集成块按裁决三转办集成宿主)
{"path": "res://tests/unit/core/minesweeper_rules_test.gd", "expect": 0}, # 排雷规则(装板不变量/挖的 face 规则/揭示推进/封印/目录外拒)
{"path": "res://tests/unit/core/config_db_resolver_test.gd", "expect": 0}, # 配置解析(ceil 偏难语义/行集行序/难度 clamp)
{"path": "res://tests/unit/core/config_db_cross_validator_test.gd", "expect": 0}, # 跨表校验(正向零错/悬空 skill_id/side 反向)
{"path": "res://tests/unit/presentation/hud_presenter_test.gd", "expect": 0}, # HUD 投影文案(逐拒绝原因/兜底不空)
{"path": "res://tests/unit/presentation/battle_narrator_test.gd", "expect": 0}, # 战斗叙述(数值带入/胜负互异/收束态恒定)
{"path": "res://tests/unit/presentation/type_glow_test.gd", "expect": 0}, # 未开格类型微光数据面(九型归组封闭/清单外显式失败/冻结色相只准动明度/α170·带宽cell÷8锚定/合成空间阶梯单调+模型CR≥1.20/probe API 形状)
{"path": "res://tests/unit/presentation/theme_projection_test.gd", "expect": 0}, # 呈现投影与主题宿主(微光数据面/战斗日志行协议/全局主题与按钮五态/纹理过滤白名单/对比度墨色锚/元素细节源码锚/诊断钮退役源码契约/状态面板与生涯页只读投影——批 B 自 smoke_all 迁入;试玩记录仪整套按裁决七转办集成宿主)
]
+1 -1
View File
@@ -3,7 +3,7 @@
# 本件现宿主零断言——smoke_all.gd 的断言按「只剪切禁改写」纪律迁入(断言迁移批执行,
# 本批只落宿主薄壳,跑通即绿)。迁移完成后 tools/run_system_test.py 缺省 --script 改指本件。
# 协议照单元侧惯例:extends SceneTree 获得 quit(code);--script 模式不加载 autoload,
# 断言夹具显式实例化 ConfigDB/RngService 构造注入;摘要行由 tools/run_system_test.py 三闸判定
# 运行形态=--script 直跑+autoload 分派闸(ConfigDB/SaveManager 在座才进主断言面——闸 01 实跑 65 项 0 失败实证在案);摘要行由 tools/run_system_test.py 三闸判定
# (退出码+摘要失败数+脚本错误扫描)。
# 运行:python tools/run_system_test.py --script res://tests/system_test.gd
extends SceneTree
@@ -440,7 +440,7 @@ func _smoke_attack_chain_fresh_pool() -> void:
var ticks: Array = [] # {baseline, attach, roll, tier, crit, tick}——敌行动伤害行重建
settlement.step_row.connect(func(row):
if row.type == "damage" and row.source_class == "enemy_action":
ticks.append({"baseline": int(row.baseline), "attach": int(row.get("attach", 0)), "roll": int(row.roll),
ticks.append({"baseline": int(row.baseline), "attach": int(row.get("rider", 0)), "roll": int(row.roll),
"tier": int(row.tier), "crit": bool(row.get("crit", false)), "tick": int(row.value)}))
Kit.check("fresh 池回归战斗启动(Boss 行)",
settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}))
File diff suppressed because it is too large. Load diff
@@ -33,6 +33,12 @@ func _init() -> void:
push_error(" [%s] %s: %s" % [d.severity, d.category, d.message])
# 套执行序沿源编排序
_smoke_seal_ap_exhausted()
_smoke_clear_reward_chain()
_smoke_refresh_provenance_guard()
_smoke_polarity_routing()
_smoke_seal_irreversible_quadrants()
_smoke_negative_dictionary()
_smoke_resource_surface()
quit(Kit.finish("seal_polarity_test"))
@@ -72,3 +78,730 @@ func _smoke_seal_ap_exhausted() -> void:
out = minesweeper.settle_batch([{"action": "seal_place", "cell": plan2.dig_cell}])
Kit.check("末点 AP 封印被受理", not out.is_empty() and bool(out[0].accepted))
Kit.check("封印动作耗尽 AP 触发轮末结算并进入第 2 轮(rounds=%s)" % str(rounds), rounds.size() >= 2)
# ==================== 清图奖励双份兑付链 / 轮内刷新 provenance 守卫 / 窗口锚行 ====================
# 清图 = 全安全格开完。清图测试用"真雷封印(零误标→完美封印)+ AutoPlayer 逐轮清图"真实流驱动;
# DifficultyTable 注入 hp_mult=10 把 Boss 血量抬到 1500,保证清图批次发生在战斗结束之前(测试台账,跑完即还原)。
const CLEAR_SMOKE_DEPTH := 4
const CLEAR_SMOKE_HERO_HP := 100000
var _regular_clear_reward_rows := 0 # 常规(非刷新图)清图的 clear_reward 行数——刷新守卫对照基线
# 板真相直读找封印目标:未开 + 指定雷/安全属性 + 存在可达已开邻格(测试允许读板真相——排雷玩法 冒烟同口径)
func _find_seal_target(minesweeper, want_mine: bool) -> Dictionary:
var revealed: Array = minesweeper.revealed_cells()
var revealed_set := {}
for c in revealed:
revealed_set[c] = true
for y in minesweeper.current_board().height:
for x in minesweeper.current_board().width:
var c := Vector2i(x, y)
if minesweeper.is_revealed(c) or minesweeper.has_seal(c):
continue
if minesweeper.current_board().is_mine(c) != want_mine:
continue
for n in minesweeper.current_board().neighbors8(c):
if revealed_set.has(n) and (minesweeper.compute_move_path(n).size() > 0 or n == minesweeper.hero_pos()):
return {"seal_cell": c, "hero_cell": n}
return {}
# ---- 封印不可逆四情况冒烟(禁撤封印 + 挖封印格兑付改判)----
# 四情况:① 插危险格不动 = 标中奖励照常;② 插危险格再挖开 = 只受挖雷一层惩罚(mine_hit 照常)标中不发;
# ③ 插安全格不动 = 标空惩罚照常;④ 插安全格再挖开 = 零惩罚零奖励(正向兑付作废、标空不判)。
# 拒绝面:seal_pull 出目录(out_of_catalog);重复封印仍 seal_present 拒绝。
# 场景 A(④ + 拒绝面):标安全格 → seal_pull 拒绝 → 重复封印拒绝 → 直接挖开 → 收轮。
# 场景 B(②):标危险格 → 直接挖开 → 收轮。
# 场景 C(①③):标安全格 + 标危险格 → 不挖收轮 → 标空/标中判定照常。
func _auto_play_round(settlement, out: Dictionary) -> void:
var minesweeper = settlement.minesweeper
var action_economy = settlement.action_economy
var start_round: int = settlement.round_index
# 确定性自动玩家:挖最近的已探明邻接未开格;无可挖则停手(结算窗口由信号回调提交)
var guard := 400
while action_economy.phase == "Active" and settlement.round_index == start_round and guard > 0 and out.result == "":
guard -= 1
var plan := AutoPlayer.choose_dig(minesweeper)
if plan.is_empty():
break
if plan.hero_cell != minesweeper.hero_pos():
minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}])
if action_economy.phase != "Active" or settlement.round_index != start_round:
break
minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
if action_economy.phase == "Active" and settlement.round_index == start_round and out.result == "":
minesweeper.settle_batch([{"action": "stop"}])
# ---- 行动经济·轮内刷新冒烟(方案 A:Active 相位当场换图继续挖)----
func _run_battle(L: int, D: int, axes: Dictionary = {}) -> Dictionary:
# 每遍全新 RngService(同 master 种子)——回放断言的隔离前提(地图战斗资源 AC"两套全新 RngService+地图战斗资源"同款)
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(42)
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
var out := {"stream": [], "events": [], "rounds": 0, "result": "", "terminated": false}
settlement.step_row.connect(func(row): out.stream.append(row))
settlement.battle_finished.connect(func(payload): out.result = payload.get("result", ""))
settlement.minesweeper.cell_revealed.connect(func(evt): out.events.append(evt))
settlement.minesweeper.seal_judged.connect(func(evt): out.events.append(evt))
settlement.minesweeper.round_end_board.connect(func(p): out.events.append({"ev": "round_end_board", "path_tag": p.path_tag}))
settlement.action_economy.round_end_stopped.connect(func(p): out.events.append({"ev": "round_end_stopped", "path_tag": p.path_tag}))
var enc := {"level_index": L, "node_depth": D, "hero_hp": _db.hero.max_hp}
enc.merge(axes, true) # 两根类型轴注入尾参(缺省零注入=回退面)
if not settlement.begin_battle(enc):
out.stream.append({"type": "verdict", "value": "begin_failed"})
return out
var max_rounds := 40
while out.result == "" and out.rounds < max_rounds:
out.rounds += 1
_auto_play_round(settlement, out)
if out.result == "":
out.result = "timeout"
out.terminated = out.result == "victory" or out.result == "defeat"
return out
# 清图驱动(真实流):标一面真雷封印(完美前置)→ 逐轮 AutoPlayer 挖到轮末;board_seq 递增 = 清图换图。
# from_refresh=true 时先三轮停手攒 RAP、第 4 轮轮内刷新换图,再清刷新图。
func _run_full_clear(seed_val: int, from_refresh: bool, rows: Array) -> Dictionary:
_db.difficulty.rows.append({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hp_mult": 10.0, "atk_mult": 1.0})
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(seed_val)
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
settlement.step_row.connect(func(row): rows.append(row))
var res := {"cleared": false, "refreshed": false, "rounds": 0}
if not settlement.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "node_type": "boss",
"hero_hp": CLEAR_SMOKE_HERO_HP, "hero_max_hp": CLEAR_SMOKE_HERO_HP}):
_db.difficulty.rows.clear()
return res
if from_refresh:
for i in range(3):
settlement.minesweeper.settle_batch([{"action": "stop"}])
res["refreshed"] = settlement.player_request_refresh()
var drive := _drive_clear(settlement)
res["cleared"] = drive.cleared
res["rounds"] = drive.rounds
_db.difficulty.rows.clear() # 测试台账还原(后续冒烟基线)
return res
func _drive_clear(settlement) -> Dictionary:
var seal_done := false
var rounds := 0
while rounds < 60:
rounds += 1
var ms = settlement.minesweeper
if not seal_done:
var t := _find_seal_target(ms, true)
if not t.is_empty():
if t.hero_cell != ms.hero_pos():
ms.settle_batch([{"action": "move", "cell": t.hero_cell}])
ms.settle_batch([{"action": "seal_place", "cell": t.seal_cell}])
seal_done = true # 一面真雷封印 = 完美封印前半(零误标),后半靠 AutoPlayer 清图
var seq_before: int = int(settlement.diagnostic_snapshot().board_seq)
var out := {"result": "", "rounds": 0, "terminated": false, "stream": []}
_auto_play_round(settlement, out)
if int(settlement.diagnostic_snapshot().board_seq) > seq_before:
return {"cleared": true, "rounds": rounds} # 清图换图 = board_seq 递增
if settlement.battle_result != "":
return {"cleared": false, "rounds": rounds}
return {"cleared": false, "rounds": rounds}
func _rows_of_type(rows: Array, t: String) -> Array:
var out: Array = []
for r in rows:
if String(r.get("type", "")) == t:
out.append(r)
return out
func _has_source_row(rows: Array, t: String, source: String, batch_key: String = "") -> bool:
for r in rows:
if String(r.get("type", "")) != t or String(r.get("source_class", "")) != source:
continue
if batch_key == "" or String(r.get("batch_key", "")) == batch_key:
return true
return false
func _has_effect_id(rows: Array, effect_id: String) -> bool:
for r in rows:
if String(r.get("effect_id", "")) == effect_id:
return true
return false
# 手搓战斗状态袋单点(7 处字面量收在一处;_autoplay_battle 直接消费的最小字段族。
# 大字段族同构袋仍走 _adv_st()——_drive_level/_autoplay_battle 全字段消费面)
const POSITIVE_EFFECTS := ["attack_up", "ac_up", "weaken", "gp_gain"]
const NEGATIVE_EFFECTS := ["enrage", "off_guard", "sapped", "alert", "gp_loss"]
# 构造验收谓词(hunt 用——种子按行为面扫描:四触达 + 标空封印负向兑付行在案)
func _polarity_probe_accepts(probe: Dictionary) -> bool:
if not (bool(probe["seal_hit_done"]) and bool(probe["seal_miss_done"])):
return false
if int(probe["mine_events"]) < 1 or int(probe["safe_events"]) < 1:
return false
var miss_rows := _rows_at(probe["rows"], "seal_miss", probe["miss_cell"])
if miss_rows.is_empty():
return false
for r in miss_rows:
if String(r.get("type", "")) == "seal_miss":
continue
var eff := String(r.get("effect_id", ""))
if eff != "" and not NEGATIVE_EFFECTS.has(eff):
return false
return true
# 极性路由探针驱动:标 1 面真雷封印(标中)+ 1 面安全格封印(标空)→ AutoPlayer 挖到战斗终局(多轮样本)。
# 收集全部 cell_revealed / seal_judged 事件与结算行;返回格子集合与封印格坐标。
func _run_polarity_probe(seed_val: int) -> Dictionary:
_db.difficulty.rows.append({"level_index": 1, "node_depth": 0, "hp_mult": 10.0, "atk_mult": 0.5}) # 抬敌血压低敌攻——多轮挖掘样本不被终局截断
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(seed_val)
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
var probe := {"rows": [], "safe_cells": {}, "mine_cells": {}, "hit_cell": [], "miss_cell": [],
"mine_events": 0, "safe_events": 0, "seal_hit_done": false, "seal_miss_done": false}
settlement.step_row.connect(func(row): probe["rows"].append(row))
settlement.minesweeper.cell_revealed.connect(func(evt):
if String(evt.get("channel", "")) == "initial":
return
if evt.kind == "mine":
probe["mine_cells"][evt.cell] = true
probe["mine_events"] = int(probe["mine_events"]) + 1
elif evt.kind == "safe":
probe["safe_cells"][evt.cell] = true
probe["safe_events"] = int(probe["safe_events"]) + 1)
settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 300, "hero_max_hp": 300})
var ms = settlement.minesweeper
# 封印 1:真雷(标中 → 正向);封印 2:安全格(标空 → 负向)
var mine_t := _find_seal_target(ms, true)
var safe_t := _find_seal_target(ms, false)
if not mine_t.is_empty():
if mine_t.hero_cell != ms.hero_pos():
ms.settle_batch([{"action": "move", "cell": mine_t.hero_cell}])
ms.settle_batch([{"action": "seal_place", "cell": mine_t.seal_cell}])
probe["hit_cell"] = [mine_t.seal_cell.x, mine_t.seal_cell.y]
probe["seal_hit_done"] = true
if not safe_t.is_empty():
if safe_t.hero_cell != ms.hero_pos():
ms.settle_batch([{"action": "move", "cell": safe_t.hero_cell}])
ms.settle_batch([{"action": "seal_place", "cell": safe_t.seal_cell}])
probe["miss_cell"] = [safe_t.seal_cell.x, safe_t.seal_cell.y]
probe["seal_miss_done"] = true
# 挖到终局(封印挡链不影响 AutoPlayer——choose_dig 跳过封印格)
var out := {"result": "", "rounds": 0, "terminated": false, "stream": []}
var guard := 40
while settlement.battle_result == "" and guard > 0:
guard -= 1
_auto_play_round(settlement, out)
_db.difficulty.rows.clear() # 测试台账还原
return probe
func _rows_at(rows: Array, source_class: String, cell) -> Array:
var out: Array = []
for r in rows:
if String(r.get("source_class", "")) == source_class and r.get("origin_cell", [9, 9]) == cell:
out.append(r)
return out
# 负向字典逐型直验(类型化兑付行协议):找真实板上各型非封锁格,直调 _grant_routed 负向路由,
# 断言每型的兑付形态(enrage/off_guard/sapped 效果行、heal 直伤行、blast 警觉行、action 行动点排队)。
func _smoke_clear_reward_chain() -> void:
print("--- 清图奖励·双份兑付链(完美封印 / 零兑付 / 第二份被拒 / 高光记实)---")
# ① 真实流完美封印:标中一面真雷封印 + 零误标 + 清全图 → grant_clear 身份(clear_blast)双份真实兑付
var rows: Array = []
var run := _run_full_clear(20260915, false, rows)
Kit.check("① 完美封印清图达成(%d 轮)" % int(run.rounds), bool(run.cleared))
var dmg := []
for r in rows:
if String(r.get("type", "")) == "damage" and String(r.get("source_class", "")) == "clear_reward":
dmg.append(r)
var hl := _rows_of_type(rows, "highlight")
_regular_clear_reward_rows = dmg.size()
Kit.check("① 清图奖励双份真实兑付(clear_reward 伤害行 ×%d——clear_blast 身份两份各打一发)" % dmg.size(), dmg.size() == 2)
if dmg.size() == 2:
Kit.check("① 第二份按身份类型走真实兑付(首份『清图奖励』/ 第二份『完美第二份』)", String(dmg[0].display_name).contains("清图奖励") and String(dmg[1].display_name).contains("完美第二份"))
Kit.check("④ 高光行 value = 实际有效份数 2(不记名义值;行数 %d)" % hl.size(), hl.size() == 1 and int(hl[0].value) == 2)
if hl.size() == 1:
Kit.check("高光标记 = perfect_sweep", String(hl[0].get("highlight", "")) == "perfect_sweep")
# ②/③ 弱形式:正常战斗流不可达的零兑付分支(见函数头注)
_smoke_clear_reward_zero_paths()
# 弱形式登记:正常战斗流下"授段时刻无存活敌"不可达——敌全灭当轮判定段即判胜并终止战斗,而清图批在判定段之前,
# 恒有存活敌;故本组直调编排私有面构造状态,只断言"路径存在 + 行为符合契约"(构造手法见各行注释)。
# 弱形式登记:正常战斗流下"授段时刻无存活敌"不可达——敌全灭当轮判定段即判胜并终止战斗,而清图批在判定段之前,
# 恒有存活敌;故本组直调编排私有面构造状态,只断言"路径存在 + 行为符合契约"(构造手法见各行注释)。
func _smoke_clear_reward_zero_paths() -> void:
# ②-a 首份零兑付判定(无存活敌)→ first_ok=false + 未生效行
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(20260915)
var s = load("res://core/battle_orchestrator.gd").new(_db, rng)
var rows: Array = []
s.step_row.connect(func(row): rows.append(row))
Kit.check("零兑付构造战斗启动", s.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80, "node_type": "boss"}))
for u in s._enemy_system.all_enemies():
s._enemy_system.apply_damage(u, int(u.hp)) # 测试通道:构造"无存活敌"(弱形式——全灭而非 despawn)
var snap = s._reward_economy.grant_clear()
Kit.check("清图槽身份 = blast 型(clear_default 唯一活行 clear_blast——第二份同身份兑付)", snap != null and String(snap.cell_type) == "blast")
rows.clear()
var first_ok: bool = s._grant_clear_identity(snap, 1, "(清图奖励)")
Kit.check("② 即时类首份零兑付(无存活敌)→ first_ok=false", not first_ok)
Kit.check("② 零兑付产出 reward_ineffective 行(batch_key=clear_blast)", _has_source_row(rows, "reward_ineffective", "clear_reward", "clear_blast"))
# ②-b 高光守卫:perfect 谓词成立但首份零兑付 → 高光行不发(不记名义值)
var rng_b = load("res://core/rng_service.gd").new()
rng_b.set_master_seed(20260915)
var s_b = load("res://core/battle_orchestrator.gd").new(_db, rng_b)
var rows_b: Array = []
s_b.step_row.connect(func(row): rows_b.append(row))
Kit.check("高光守卫构造战斗启动", s_b.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80, "node_type": "boss"}))
for u in s_b._enemy_system.all_enemies():
s_b._enemy_system.apply_damage(u, int(u.hp))
s_b._perfect_hit_true = 1 # 测试通道:构造完美谓词成立(真值在正常流由封印判定累加)
s_b._perfect_hit_false = 0
rows_b.clear()
s_b._grant_clear_reward(1)
Kit.check("② _grant_clear_reward 守卫路径重走首份 → 未生效行仍在案", _has_source_row(rows_b, "reward_ineffective", "clear_reward", "clear_blast"))
Kit.check("② 首份零兑付 → 高光行不发(不记名义值 2)", _rows_of_type(rows_b, "highlight").is_empty())
# ③ 第二份被拒:唯一存活敌 HP ≤ 首份 aoe 量 → 首份击杀 → 第二份无存活敌(zero + 未生效行)
var rng2 = load("res://core/rng_service.gd").new()
rng2.set_master_seed(20260915)
var s2 = load("res://core/battle_orchestrator.gd").new(_db, rng2)
var rows2: Array = []
s2.step_row.connect(func(row): rows2.append(row))
Kit.check("第二份被拒构造战斗启动", s2.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80, "node_type": "boss"}))
var boss = s2._enemy_system.all_enemies()[0]
boss.hp = 1 # 测试通道:压低至首份 aoe 必杀(弱形式——正常流 Boss 血量不可控)
s2._perfect_hit_true = 1
s2._perfect_hit_false = 0
rows2.clear()
s2._grant_clear_reward(1)
Kit.check("③ 首份 aoe 真实兑付(击杀唯一存活敌)", _has_source_row(rows2, "damage", "clear_reward"))
Kit.check("③ 第二份被拒产出 reward_ineffective 行(零兑付)", _has_source_row(rows2, "reward_ineffective", "clear_reward", "clear_blast"))
var hl2 := _rows_of_type(rows2, "highlight")
Kit.check("④ 高光行 value = 实际有效份数 1(不记名义值 2)", hl2.size() == 1 and int(hl2[0].value) == 1)
# 轮内刷新 provenance 守卫:刷新换来的图同轮清图不发清图奖励(_board_from_midround_refresh 守卫)
# 轮内刷新 provenance 守卫:刷新换来的图同轮清图不发清图奖励(_board_from_midround_refresh 守卫)
func _smoke_refresh_provenance_guard() -> void:
print("--- 轮内刷新 provenance 守卫(刷新图清图不发清图奖励)---")
var rows: Array = []
var run := _run_full_clear(20260916, true, rows)
Kit.check("轮内刷新受理(三轮停手攒 RAP 后 Active 相位发起)", bool(run.refreshed))
Kit.check("刷新图清图达成(%d 轮)" % int(run.rounds), bool(run.cleared))
var cr := 0
var hl := 0
for r in rows:
if String(r.get("source_class", "")) == "clear_reward":
cr += 1
if String(r.get("type", "")) == "highlight":
hl += 1
Kit.check("刷新图清图 → 无 clear_reward 行(守卫在位——实测 %d 行)" % cr, cr == 0)
Kit.check("刷新图清图 → 无高光行(守卫在位——实测 %d 行)" % hl, hl == 0)
Kit.check("对照基线:常规换图清图有 clear_reward 行(×%d——守卫非全局静默)" % _regular_clear_reward_rows, _regular_clear_reward_rows > 0)
# ==================== 格面资源批新增覆盖(极性路由 / 负向字典 / 资源面 / 五段全序) ====================
# 极性路由断言(战斗结算 验收):挖安全格/封印命中只出正向、挖危险格/标错封印只出负向、兑付行指回来源格。
# 正向字典效果集 = {attack_up, ac_up, weaken}
# + spark_aoe/heal_minor(flat)+ gp_gain(GP 袋,escrow 缓冲路由键非增益容器行);
# 负向字典效果集 = {enrage, off_guard, sapped}(percent)+ 直伤(heal flat)/ alert 警觉(blast percent)
# + gp_loss(破财,escrow 缓冲路由键)。
func _smoke_polarity_routing() -> void:
print("--- 极性路由(挖安全格正向 / 挖危险格负向 / 标中正向 / 标空负向)---")
# 战斗随机值面随全局序漂移——构造种子按行为面扫描(谓词见 _polarity_probe_accepts)
var probe := {}
var polarity_seed := -1
for s in [20260918, 20260919, 20260920, 20260921, 42, 4242, 7, 100, 20260922, 123, 2024, 31337, 555, 1111, 2222, 20260923, 999, 777]:
probe = _run_polarity_probe(s)
if _polarity_probe_accepts(probe):
polarity_seed = s
break
Kit.check("极性路由构造种子可达(seed=%d——并流后行为面重锚)" % polarity_seed, polarity_seed > 0)
Kit.check("构造触达:标中真雷 + 标空安全格均入队", bool(probe["seal_hit_done"]) and bool(probe["seal_miss_done"]))
Kit.check("构造触达:挖到危险格 ≥1(%d)" % int(probe["mine_events"]), int(probe["mine_events"]) >= 1)
Kit.check("构造触达:挖到安全格 ≥1(%d)" % int(probe["safe_events"]), int(probe["safe_events"]) >= 1)
# 挖到的安全格:只出正向——其全部来源行必存在且 effect 行 ∈ 正向集
# (action 型排队行 effect_id=ap_delta 正负两极共用路由键——按行值符号辨极性:正向格恒 +power)
var safe_all_pos := true
for e in probe["safe_cells"].keys():
var cell_arr: Array = [e.x, e.y]
var rows := _rows_at(probe["rows"], "safe_cell", cell_arr)
if rows.is_empty():
safe_all_pos = false # 每个资源格必有 ≥1 兑付行(每格必有货 → 每格必有行)
for r in rows:
var eff := String(r.get("effect_id", ""))
if eff == "ap_delta":
if int(r.get("value", 0)) <= 0:
safe_all_pos = false
continue
if eff != "" and not POSITIVE_EFFECTS.has(eff):
safe_all_pos = false
Kit.check("挖安全格 → 只出正向兑付(效果行 ∈ 正向字典 + 每格 ≥1 行——%d 格)" % probe["safe_cells"].size(), safe_all_pos)
# 挖到的危险格:只出负向——来源行必存在且 effect 行 ∈ 负向集(alert = blast 负向;heal 负向 = damage 行;
# action 型排队行同按行值符号辨极性:危险格恒 −power)
var mine_all_neg := true
for e in probe["mine_cells"].keys():
var cell_arr: Array = [e.x, e.y]
var rows := _rows_at(probe["rows"], "mine_hit", cell_arr)
if rows.is_empty():
mine_all_neg = false
for r in rows:
var eff := String(r.get("effect_id", ""))
if eff == "ap_delta":
if int(r.get("value", 0)) >= 0:
mine_all_neg = false
continue
if eff != "" and not NEGATIVE_EFFECTS.has(eff):
mine_all_neg = false
Kit.check("挖危险格 → 只出负向兑付(效果行 ∈ 负向字典 + 每格 ≥1 行——%d 格)" % probe["mine_cells"].size(), mine_all_neg)
# 封印命中:该危险格正向兑付行(seal_hit 来源)+ 效果 ∈ 正向字典
var hit_ok := false
for r in probe["rows"]:
if String(r.get("source_class", "")) == "seal_hit" and r.get("origin_cell", [0, 0]) == probe["hit_cell"]:
hit_ok = true
var eff := String(r.get("effect_id", ""))
if eff == "ap_delta":
if int(r.get("value", 0)) <= 0:
hit_ok = false
continue
if eff != "" and not POSITIVE_EFFECTS.has(eff):
hit_ok = false
Kit.check("封印命中 → 该危险格资源的正向兑付行(origin 指回危险格,效果 ∈ 正向字典;action 型按行值符号辨极性)", hit_ok)
# 条款3 条件二封印溢价断言(标中兑付行数值 = 同型正向 ×(1+δ)):
# δ 读 settlement.seal_hit_premium(125 provisional);九型正向行值全固定(唯一区间行 clear_blast
# 系清图槽不入格池)⇒ 逐型期望全确定。整数量级四舍五入域注记:ac_up/action 1 点量纲取整后不变
# =整数粒度地板——等号压线判读在当量聚合面(audit 工具/registry clause3_inequality),非断言面。
var prem_pct: int = int(_db.settlement.seal_hit_premium)
Kit.check("封印溢价配置在位(seal_hit_premium=125 provisional——形态 a 兑付溢价)", prem_pct == 125)
var prem_rows := 0
var prem_ok := true
for r in probe["rows"]:
if String(r.get("source_class", "")) != "seal_hit" or String(r.get("highlight", "")) != "seal_hit":
continue
prem_rows += 1
var rtype := String(r.get("type", ""))
var v := int(r.get("value", 0))
var eff := String(r.get("effect_id", ""))
if rtype == "effect":
if eff == "gp_gain":
prem_ok = prem_ok and v == int(round(3 * prem_pct / 100.0)) # GP 袋 3→4
elif eff == "ap_delta":
prem_ok = prem_ok and v == 1 # +1AP:1 点量纲(round(1.25)=1——整数粒度地板)
else:
var mag: int = int(_db.status_effect_row(eff).get("magnitude", -999))
prem_ok = prem_ok and v == int(round(mag * prem_pct / 100.0)) # 增伤50→63/削弱65→81/护甲1→1
elif rtype == "heal":
# 急救 2→3(clamp 封顶只降不升:满血走 reward_ineffective 不入本行——区间断言)
prem_ok = prem_ok and v >= 1 and v <= int(round(2 * prem_pct / 100.0))
elif rtype == "damage":
var at := String(r.get("attack_type", ""))
if at == "":
prem_ok = prem_ok and v == int(round(1 * prem_pct / 100.0)) # 爆裂粉尘 1→1(群伤行值=表值直付)
else:
# 穿刺/魔法 2→3(行值=落地伤=基准+克制点数——反推基准核对;damage_floor 兜底下成立)
prem_ok = prem_ok and (v - int(r.get("effectiveness", 0))) == int(round(2 * prem_pct / 100.0))
Kit.check("标中兑付溢价(×%d 行逐型=同型正向 ×%d/100——等号压线注记:δ=1.25 时条件二 E[标中]=1.25·E_dig 恰等成立)" % [prem_rows, prem_pct], prem_rows >= 1 and prem_ok)
# 标空不乘溢价:负向直伤行(反噬6/陷阱8/魔法反噬8 固定表值)零溢价污染——溢价值挂标中正向单点
var miss_flat_ok := true
for r in _rows_at(probe["rows"], "seal_miss", probe["miss_cell"]):
if String(r.get("type", "")) == "damage":
miss_flat_ok = miss_flat_ok and int(r.get("value", 0)) in [6, 8]
Kit.check("标空惩罚零溢价(负向直伤行=固定表值原值——溢价只挂标中正向)", miss_flat_ok)
# 标空封印:归因行 + 该安全格负向兑付行(seal_miss 来源)
var miss_rows := _rows_at(probe["rows"], "seal_miss", probe["miss_cell"])
var miss_neg_ok := false
for r in miss_rows:
if String(r.get("type", "")) == "seal_miss":
miss_neg_ok = true
else:
var eff := String(r.get("effect_id", ""))
if eff != "" and not NEGATIVE_EFFECTS.has(eff):
miss_neg_ok = false
Kit.check("标空封印 → 封印未中归因行 + 该安全格资源负向兑付(同格同批)", not miss_rows.is_empty() and miss_neg_ok)
# 抵消过滤既有语义:封印后被撤封印不判(发射面基线过滤)——由排雷玩法 冒烟与封印判定基线覆盖,此处核兑付行零泄漏
var seal_srcs := {}
for r in probe["rows"]:
if String(r.get("source_class", "")) in ["seal_hit", "seal_miss"]:
seal_srcs[r.get("origin_cell")] = true
Kit.check("封印判定兑付仅封印判定批两封印在案(撤封印抵消过滤零泄漏——%d 格)" % seal_srcs.size(), int(seal_srcs.size()) <= 2)
# 极性路由探针驱动:标 1 面真雷封印(标中)+ 1 面安全格封印(标空)→ AutoPlayer 挖到战斗终局(多轮样本)。
# 收集全部 cell_revealed / seal_judged 事件与结算行;返回格子集合与封印格坐标。
# ---- 封印不可逆四情况冒烟(禁撤封印 + 挖封印格兑付改判)----
# 四情况:① 插危险格不动 = 标中奖励照常;② 插危险格再挖开 = 只受挖雷一层惩罚(mine_hit 照常)标中不发;
# ③ 插安全格不动 = 标空惩罚照常;④ 插安全格再挖开 = 零惩罚零奖励(正向兑付作废、标空不判)。
# 拒绝面:seal_pull 出目录(out_of_catalog);重复封印仍 seal_present 拒绝。
# 场景 A(④ + 拒绝面):标安全格 → seal_pull 拒绝 → 重复封印拒绝 → 直接挖开 → 收轮。
# 场景 B(②):标危险格 → 直接挖开 → 收轮。
# 场景 C(①③):标安全格 + 标危险格 → 不挖收轮 → 标空/标中判定照常。
func _smoke_seal_irreversible_quadrants() -> void:
print("--- 封印不可逆四情况(禁撤封印 + 挖封印格兑付改判)---")
# 场景 A:安全格直挖(情况④)+ seal_pull/重复封印拒绝面
var rng_a = load("res://core/rng_service.gd").new()
rng_a.set_master_seed(20260919)
var settle_a = load("res://core/battle_orchestrator.gd").new(_db, rng_a)
var rows_a: Array = []
settle_a.step_row.connect(func(row): rows_a.append(row))
Kit.check("四情况 A:开局受理", settle_a.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}))
var ms_a = settle_a.minesweeper
var t_a := _find_seal_target(ms_a, false)
Kit.check("四情况 A:找到安全格封印目标", not t_a.is_empty())
if not t_a.is_empty():
if t_a.hero_cell != ms_a.hero_pos():
ms_a.settle_batch([{"action": "move", "cell": t_a.hero_cell}])
Kit.check("四情况 A:封印受理", ms_a.settle_batch([{"action": "seal_place", "cell": t_a.seal_cell}])[0].accepted)
var pull_out: Array = ms_a.settle_batch([{"action": "seal_pull", "cell": t_a.seal_cell}])
Kit.check("四情况 A:seal_pull 出目录拒绝(撤封印退役)", not pull_out.is_empty() and not bool(pull_out[0].get("accepted", true)) and String(pull_out[0].get("sub_reason", "")) == "out_of_catalog")
var dup_place: Array = ms_a.settle_batch([{"action": "seal_place", "cell": t_a.seal_cell}])
Kit.check("四情况 A:重复封印仍被拒(seal_present)", not dup_place.is_empty() and not bool(dup_place[0].get("accepted", true)) and String(dup_place[0].get("sub_reason", "")) == "seal_present")
var dig_a: Array = ms_a.settle_batch([{"action": "dig", "cell": t_a.seal_cell}])
Kit.check("四情况 A:封印格直接挖开被受理(不再须先撤封印)", not dig_a.is_empty() and bool(dig_a[0].get("accepted", false)))
Kit.check("四情况 A:封印随挖掘隐式退役(has_seal = false)", not ms_a.has_seal(t_a.seal_cell))
ms_a.settle_batch([{"action": "stop"}])
var target_a: Array = [t_a.seal_cell.x, t_a.seal_cell.y]
var miss_a := 0
var grant_a := 0
var void_a := 0
for row in rows_a:
if row.get("origin_cell", [0, 0]) != target_a:
continue
if row.get("type", "") == "seal_miss" or String(row.get("source_class", "")) == "seal_miss":
miss_a += 1
if String(row.get("source_class", "")) == "safe_cell" and row.get("type", "") != "reward_ineffective":
grant_a += 1
if row.get("type", "") == "reward_ineffective":
void_a += 1
Kit.check("四情况 A:标空判定零兑付(×%d——封印已擦除不入轮末基线)" % miss_a, miss_a == 0)
Kit.check("四情况 A:安全格正向兑付作废(×%d——零奖励)" % grant_a, grant_a == 0)
Kit.check("四情况 A:兑付作废归因行恰一次(×%d——零惩罚零奖励留痕)" % void_a, void_a == 1)
# 场景 B:危险格直挖(情况②)
var rng_b = load("res://core/rng_service.gd").new()
rng_b.set_master_seed(20260919)
var settle_b = load("res://core/battle_orchestrator.gd").new(_db, rng_b)
var rows_b: Array = []
settle_b.step_row.connect(func(row): rows_b.append(row))
Kit.check("四情况 B:开局受理", settle_b.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}))
var ms_b = settle_b.minesweeper
var t_b := _find_seal_target(ms_b, true)
Kit.check("四情况 B:找到危险格封印目标", not t_b.is_empty())
if not t_b.is_empty():
if t_b.hero_cell != ms_b.hero_pos():
ms_b.settle_batch([{"action": "move", "cell": t_b.hero_cell}])
Kit.check("四情况 B:封印受理", ms_b.settle_batch([{"action": "seal_place", "cell": t_b.seal_cell}])[0].accepted)
var dig_b: Array = ms_b.settle_batch([{"action": "dig", "cell": t_b.seal_cell}])
Kit.check("四情况 B:封印危险格直接挖开被受理", not dig_b.is_empty() and bool(dig_b[0].get("accepted", false)))
Kit.check("四情况 B:危险格已揭示(挖雷本体照常)", ms_b.is_revealed(t_b.seal_cell) and ms_b.current_board().is_mine(t_b.seal_cell))
ms_b.settle_batch([{"action": "stop"}])
var target_b: Array = [t_b.seal_cell.x, t_b.seal_cell.y]
var mine_hit_b := 0
var hit_b := 0
for row in rows_b:
if row.get("origin_cell", [0, 0]) != target_b:
continue
if String(row.get("source_class", "")) == "mine_hit":
mine_hit_b += 1
if row.get("type", "") == "seal_hit" or String(row.get("source_class", "")) == "seal_hit":
hit_b += 1
Kit.check("四情况 B:挖雷惩罚照常兑付(×%d——只受挖雷一层惩罚)" % mine_hit_b, mine_hit_b >= 1)
Kit.check("四情况 B:标中不发(×%d——封印已擦除不判)" % hit_b, hit_b == 0)
# 场景 C:不挖收轮(①③判定照常)
var rng_c = load("res://core/rng_service.gd").new()
rng_c.set_master_seed(20260919)
var settle_c = load("res://core/battle_orchestrator.gd").new(_db, rng_c)
var rows_c: Array = []
settle_c.step_row.connect(func(row): rows_c.append(row))
Kit.check("四情况 C:开局受理", settle_c.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}))
var ms_c = settle_c.minesweeper
var t_safe := _find_seal_target(ms_c, false)
var t_mine := _find_seal_target(ms_c, true)
Kit.check("四情况 C:安全格+危险格封印目标齐备", not t_safe.is_empty() and not t_mine.is_empty())
if not t_safe.is_empty() and not t_mine.is_empty():
for t in [t_safe, t_mine]:
if t.hero_cell != ms_c.hero_pos():
ms_c.settle_batch([{"action": "move", "cell": t.hero_cell}])
ms_c.settle_batch([{"action": "seal_place", "cell": t.seal_cell}])
ms_c.settle_batch([{"action": "stop"}]) # 不挖收轮——两封印均在场基线
var safe_c: Array = [t_safe.seal_cell.x, t_safe.seal_cell.y]
var mine_c: Array = [t_mine.seal_cell.x, t_mine.seal_cell.y]
var miss_c := 0
var miss_neg_c := 0
var hit_c := 0
for row in rows_c:
if row.get("origin_cell", [0, 0]) == safe_c and row.get("type", "") == "seal_miss":
miss_c += 1
if row.get("origin_cell", [0, 0]) == safe_c and String(row.get("source_class", "")) == "seal_miss" and row.get("type", "") != "seal_miss":
miss_neg_c += 1
if row.get("origin_cell", [0, 0]) == mine_c and (row.get("type", "") == "seal_hit" or String(row.get("source_class", "")) == "seal_hit"):
hit_c += 1
Kit.check("四情况 C:标空惩罚照常(归因×%d 负向兑付×%d)" % [miss_c, miss_neg_c], miss_c >= 1 and miss_neg_c >= 1)
Kit.check("四情况 C:标中奖励照常(×%d)" % hit_c, hit_c >= 1)
# ---- 试玩日志器冒烟(结算快照 JSONL + 诊断转储)----
# 负向字典逐型直验(类型化兑付行协议):找真实板上各型非封锁格,直调 _grant_routed 负向路由,
# 断言每型的兑付形态(enrage/off_guard/sapped 效果行、heal 直伤行、blast 警觉行、action 行动点排队)。
func _smoke_negative_dictionary() -> void:
print("--- 负向字典逐型直验(sapped 在位 / heal 直伤 / blast 警觉 / action 行动点)---")
# 数据面:sapped 行在位(状态表七行 + effect_pool_map 映射在位)
var sapped: Dictionary = _db.status_effect_row("sapped")
Kit.check("sapped 行在位(hero · −25 percent · dur 1 · max 16 · stack——weaken 型负向)",
not sapped.is_empty() and String(sapped.get("target_side", "")) == "hero" \
and int(sapped.get("magnitude", 0)) == -25 and int(sapped.get("duration_settlements", 0)) == 1 \
and int(sapped.get("max_stack", 0)) == 16 and String(sapped.get("stack_policy", "")) == "stack")
Kit.check("effect_pool_map 收编 sapped → hero_offense(attack_up 镜像池)", String(_db.settlement.effect_pool_map.get("sapped", "")) == "hero_offense")
# 行为面:逐型负向兑付形态(真实板 + 真实路由路径)
# heal 权重低(无 heal 板概率 ≈1/3)——单板五型齐备不再必然:
# 本断言多板扫描,取第一块五型齐备的板做行为断言。
var settlement = null
var board = null
var by_type := {} # cell_type → 首个非封锁格(query_identity 雷安同池——危险格可查)
var rows: Array = []
for seed_i in range(6):
# 每板独立 rng 实例(复用实例重播种触发 set_master_seed 的 _counter==0 守卫;
# 单板 break 会掩盖该违例,多板扫描才暴露)
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(20260919 + seed_i)
settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
rows = []
settlement.step_row.connect(func(row): rows.append(row))
if not settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp), "hero_max_hp": int(_db.hero.max_hp)}):
continue
board = settlement.minesweeper.current_board()
var r0 := {}
for c in board.compute_r0():
r0[c] = true
by_type = {}
for y in board.height:
for x in board.width:
var c := Vector2i(x, y)
if r0.has(c) or by_type.size() == 5:
continue
var ident = settlement.query_reward_identity(c)
# aux 身份不入五型轴(五型齐备=格面资源断言轴,
# aux 供给挤占不改变本断言语义;扫描排除 aux 身份格)
if ident != null and String(ident.cell_type) != RewardTable.TREASURE_ITEM_TYPE and not by_type.has(String(ident.cell_type)):
by_type[String(ident.cell_type)] = c
# break 条件=percent_map 三型齐备(aux 供给挤占下 weaken/defense 收集时机漂移,
# 直验可达性优先;五型多样性 check 保留不动)
if by_type.has("attack") and by_type.has("defense") and by_type.has("weaken"):
break
Kit.check("扫描 6 板内出现五型齐备板(%s)" % str(by_type.keys()), by_type.size() == 5)
# attack/defense/weaken:负向字典行走 apply_effect(off_guard 走 ac 轴——兑付形态不变)
var percent_map := {"attack": "enrage", "defense": "off_guard", "weaken": "sapped"}
for ctype in percent_map.keys():
if not by_type.has(ctype):
continue
rows.clear()
var snap: Dictionary = settlement._reward_economy.query_full_snapshot(by_type[ctype])
settlement._grant_routed(snap, "negative", "mine_hit", by_type[ctype], 1)
var eff: String = percent_map[ctype]
Kit.check("%s 型负向 → %s 效果行(mine_hit 来源指回该格)" % [ctype, eff], _has_source_row(rows, "effect", "mine_hit") and _has_effect_id(rows, eff))
# heal:负向 = 主角直伤(表值;不经增益状态)
if by_type.has("heal"):
rows.clear()
var hp_before: int = int(settlement.presentation_snapshot().hero.hp)
var snap_h: Dictionary = settlement._reward_economy.query_full_snapshot(by_type["heal"])
settlement._grant_routed(snap_h, "negative", "mine_hit", by_type["heal"], 1)
var hp_after: int = int(settlement.presentation_snapshot().hero.hp)
var backlash_power: int = 0
for row in _db.reward.rows:
if String(row.get("reward_id", "")) == "heal_backlash":
backlash_power = int(row.power)
Kit.check("heal 型负向 → 主角直伤 %d(%d → %d,不经增益状态)" % [backlash_power, hp_before, hp_after], hp_after == hp_before - backlash_power)
# blast:负向 = 警觉(敌方护甲等级 +1 percent 行)
if by_type.has("blast"):
rows.clear()
var snap_b: Dictionary = settlement._reward_economy.query_full_snapshot(by_type["blast"])
settlement._grant_routed(snap_b, "negative", "mine_hit", by_type["blast"], 1)
var alert_ok: bool = false
for eff_a in settlement._status_effects.query_active("enemy"):
if String(eff_a.effect_id) == "alert":
alert_ok = true
Kit.check("blast 型负向 → 警觉(敌方护甲等级 +1 状态在位)", alert_ok)
# action:负向 = 下轮行动点 −1 / 正向 = +1(排队,轮开始兑现;确定性直验不依赖板面摇号)
rows.clear()
var d0: int = settlement.action_economy.pending_ap_delta()
settlement._grant_routed({"reward_id": "ap_drain", "cell_type": "action", "polarity": "negative", "display_name": "迟滞", "power": 1}, "negative", "mine_hit", Vector2i(0, 0), 1)
Kit.check("action 型负向 → 下轮行动点 −1(排队)", settlement.action_economy.pending_ap_delta() == d0 - 1)
settlement._grant_routed({"reward_id": "ap_up", "cell_type": "action", "polarity": "positive", "display_name": "行动点", "power": 1}, "positive", "safe_open", Vector2i(0, 0), 1)
Kit.check("action 型正负抵消 → 排队净值归零", settlement.action_economy.pending_ap_delta() == d0)
# 资源面断言:chain/dig 事件恒携带 resource=true(危险格同权);initial 恒 false(封锁格);
# 封锁格查询 = null、危险格未开恒可查。
# 资源面断言:chain/dig 事件恒携带 resource=true(危险格同权);initial 恒 false(封锁格);
# 封锁格查询 = null、危险格未开恒可查。
func _smoke_resource_surface() -> void:
print("--- 格面资源资格面(危险格携带 / 封锁格无资源)---")
var run1 = _run_battle(1, 0)
var run2 = _run_battle(2, 0)
var all_events: Array = run1.events + run2.events
var digchain_bad := 0
var initial_bad := 0
var mine_with_resource := 0
var initial_count := 0
for evt in all_events:
if not evt.has("resource"):
continue # 非揭示事件(round_end 标记等)
if String(evt.get("channel", "")) == "initial":
initial_count += 1
if bool(evt.resource):
initial_bad += 1
else:
if not bool(evt.resource):
digchain_bad += 1
if evt.kind == "mine":
mine_with_resource += 1
Kit.check("chain/dig 揭示事件 resource 恒 true(雷安同池——违例 %d)" % digchain_bad, digchain_bad == 0)
Kit.check("initial 揭示事件(封锁格)resource 恒 false(×%d 事件)" % initial_count, initial_bad == 0 and initial_count >= 1)
Kit.check("危险格揭示携带资源(mine ×resource=true %d——危险格不再恒无)" % mine_with_resource, mine_with_resource >= 1)
# 封锁格无身份 / 危险格未开恒可查(未开格恒可查——秘宝 桥接结构前提)
var rng3 = load("res://core/rng_service.gd").new()
rng3.set_master_seed(20260920)
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng3)
settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)})
var board = settlement.minesweeper.current_board()
var r0_first: Variant = null
for c in board.compute_r0():
r0_first = c
break
var mine_unrevealed = null
for c in board.mines.keys():
if not settlement.minesweeper.is_revealed(c):
mine_unrevealed = c
break
Kit.check("封锁格(R₀)身份查询 = null(不入摇号清单)", settlement.query_reward_identity(r0_first) == null)
Kit.check("未开危险格身份恒可查({reward_id, cell_type, tier}——秘宝 桥接前提)", mine_unrevealed != null and settlement.query_reward_identity(mine_unrevealed) != null)
# 五段全序断言(战斗结算 验收"五段全序"+ 行型封闭 10 类):逐轮行相位序 = 授予 → 增益/减益 → 战斗 → 衰减 → 判定。
@@ -0,0 +1,312 @@
# tests/unit/battle_settlement/variant_placement_test.gd — 地形变异单元测试宿主(G6 第二宿主)
# 断言面 = 两套(批 B 断言迁移自 smoke_all.gd,G6 施工组):
# 地形变异 布置段(编排器战斗初始化第 4 步——门控一致性/布置域/数量公式/连通片上限/确定性;L1/L2 双迭代)
# 全局作用域变异(互锁负样本/门控三维/全局池空兜底回落/资源面偏斜守恒与合成序/快照投影不变)
# 协议照单元侧惯例:extends SceneTree+Kit.check 主通道;--script 模式无 autoload,
# ConfigDB 显式实例化构造注入(各套自建 RngService——源套掷点隔离形态,宿主不持统一流);
# 表加载失败不中止(沿被迁断言面的源语义——承 G2/G3/G4/G5 同款)。
# 运行:godot --headless --path game --script res://tests/unit/battle_settlement/variant_placement_test.gd
extends SceneTree
const Kit := preload("res://tests/unit/test_kit.gd")
const ConfigDBScript := preload("res://core/config_db.gd")
var _db
func _init() -> void:
_db = ConfigDBScript.new()
# 表加载失败不中止(承 G2/G3/G4/G5 同款:表损坏的红落在具体断言上——
# 诊断在此展开,后续断言按坏表实况逐条验红;collect_only=true 下成员照常赋值可评)
var load_ok: bool = _db.load_tables("res://data/tables", true)
if not load_ok:
push_error("[variant_placement_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])
# 套执行序沿源编排(基点 _run_core_smoke 调用序——本组套的相对序)
_smoke_variant_placement(1)
_smoke_variant_placement(2)
_smoke_global_variant()
quit(Kit.finish("variant_placement_test"))
# ---- 全局作用域变异 断言面(「全局池空 → region 兜底回落」路径
# ——互锁负样本/门控三维/资源面偏斜守恒与合成序/快照投影不变)----
# ---- 全局作用域变异 断言面(「全局池空 → region 兜底回落」路径
# ——互锁负样本/门控三维/资源面偏斜守恒与合成序/快照投影不变)----
# 取号计数桩(variant_place 取号断言——子类拦截计数,派生与取数语义零改动)
class _CountVariantRng extends RngServiceCore:
var variant_calls := 0
func open_stream(purpose: String) -> RandomNumberGenerator:
if purpose == "variant_place":
variant_calls += 1
return super.open_stream(purpose)
# ---- 地形变异 布置段(棋盘设置顺序第 4 步断言面)----
# 布置发生在编排器战斗初始化(install 初始展开 + roll_board 资源格之后),生成器产物零变异——
# 本段经 begin_battle 走完整初始化序后断言:门控一致性 / 布置域 / 数量公式 / 连通片上限 / 确定性。
func _smoke_variant_placement(L: int) -> void:
print("--- 地形变异 布置段(L%d)---" % L)
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(42)
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
if not settlement.begin_battle({"level_index": L, "node_depth": 0, "hero_hp": _db.hero.max_hp}):
Kit.check("L%d 布置段战斗启动失败" % L, false)
return
var b = settlement.minesweeper.current_board()
Kit.check("L%d 布置段板非空" % L, b != null)
if b == null:
return
var pool_ids: Array = []
for r in _db.variant_pool(L, String(b.board_type)):
pool_ids.append(String(r.variant_id))
var vcells: Array = b.variants.keys()
var vid0 := ""
var single_type := true
var bad_id := false
for c in vcells:
var ids: Array = b.variants[c]
if ids.size() != 1:
single_type = false
for vid in ids:
if vid0 == "":
vid0 = String(vid)
elif vid0 != String(vid):
single_type = false
if not pool_ids.has(String(vid)):
bad_id = true
Kit.check("L%d 变异 id 全在候选池(门控一致性:%s)" % [L, str(pool_ids)], not bad_id)
Kit.check("L%d 每图至多一种变异类型(单格单 id + 全图同 id)" % L, single_type)
var r0 := {}
for c in b.compute_r0():
r0[c] = true
var domain_ok := true
for c in vcells:
if r0.has(c) or b.is_mine(c):
domain_ok = false
Kit.check("L%d 变异格全在初始展开区外且不吃危险格(布置域结构保障——closed_only)" % L, domain_ok)
var cand_count := 0
for c in b.safe_cells():
if not r0.has(c):
cand_count += 1
var expect_cap: int = int(round(float(cand_count) * float(_db.variation.percent_of_cells) / 100.0)) \
+ (L - 1) * int(_db.variation.per_level_bonus)
Kit.check("L%d 变异格数 %d ≤ 公式上限 %d(可挖池百分比 + 关卡递增)" % [L, vcells.size(), expect_cap], vcells.size() <= expect_cap)
# 数量公式量随候选池缩放:L1 无关卡加成,极小池(<10 格)可 round 至 0——
# 零布置为合法态(同池空口径);非零断言为条件式,公式量>0 时才要求非零(L2/L3 关卡加成保底恒>0)
Kit.check("L%d 公式量>0 时非零布置(极小池 round 至 0 = 合法零布置——2026-09-21 基数修正批)" % L, expect_cap == 0 or vcells.size() > 0)
var pack_max: int = int(_db.variation.pack_max)
var vset := {}
for c in vcells:
vset[c] = true
var seen := {}
var pack_ok := true
for c in vcells:
if seen.has(c):
continue
seen[c] = true
var comp_size := 1
var q: Array = [c]
while q.size() > 0:
var x: Vector2i = q.pop_front()
for n in b.neighbors8(x):
if vset.has(n) and not seen.has(n):
seen[n] = true
comp_size += 1
q.append(n)
if comp_size > pack_max:
pack_ok = false
Kit.check("L%d 全部连通片 ≤ %d 格(全连通生长、2×2/1×4/1×3+1 等形)" % [L, pack_max], pack_ok)
# 布置确定性:全新 RngService 同种子重开,变异布置逐格相同(variant_place 流确定性)
var rng2 = load("res://core/rng_service.gd").new()
rng2.set_master_seed(42)
var s2 = load("res://core/battle_orchestrator.gd").new(_db, rng2)
s2.begin_battle({"level_index": L, "node_depth": 0, "hero_hp": _db.hero.max_hp})
var b2 = s2.minesweeper.current_board()
Kit.check("L%d 同种子变异布置逐格相同(variant_place 流确定性)" % L, b2 != null and _variant_dump(b2) == _variant_dump(b))
func _variant_dump(b) -> String:
var keys: Array = b.variants.keys()
keys.sort_custom(func(a, b2): return a.y * 1000 + a.x < b2.y * 1000 + b2.x)
var parts: Array = []
for c in keys:
parts.append("%d,%d:%s" % [c.x, c.y, str(b.variants[c])])
return "|".join(parts)
# ---- 完整战斗回放(自动玩家,确定性)——回放断言:同种子两遍步骤流逐行相同 ----
func _smoke_global_variant() -> void:
print("--- 全局作用域变异(门控三维 + 全局池空兜底回落 + 资源面偏斜)---")
# ① 跨字段互锁负样本(resource_face ⟺ scope=global 三条 + effect_value 新域 [0,50])
var vt = load("res://data/schemas/variation_table.gd").new()
vt.rows.assign([{"variant_id": "g1", "display_name": "负甲", "unlock_level": 2, "scope": "region",
"effect": "resource_face", "effect_value": 30, "trigger": "always", "placement": "any",
"board_types": [], "counter_items": []}])
Kit.check("互锁负样本③:resource_face + scope=region 被拒(resource_face ⇒ global)", _errs_has_cross_field(vt.validate()))
vt.rows.assign([{"variant_id": "g2", "display_name": "负乙", "unlock_level": 2, "scope": "global",
"effect": "hide_number", "effect_value": 0, "trigger": "always", "placement": "any",
"board_types": [], "counter_items": []}])
Kit.check("互锁负样本④:scope=global + effect=hide_number 被拒(global ⇒ resource_face)", _errs_has_cross_field(vt.validate()))
vt.rows.assign([{"variant_id": "g3", "display_name": "负丙", "unlock_level": 2, "scope": "global",
"effect": "resource_face", "effect_value": 30, "trigger": "on_open", "placement": "any",
"board_types": [], "counter_items": []}])
Kit.check("互锁负样本⑤:scope=global + trigger=on_open 被拒(global ⇒ always 整图恒在)", _errs_has_cross_field(vt.validate()))
vt.rows.assign([{"variant_id": "g4", "display_name": "负丁", "unlock_level": 2, "scope": "global",
"effect": "resource_face", "effect_value": 30, "trigger": "always", "placement": "closed_only",
"board_types": [], "counter_items": []}])
Kit.check("互锁负样本⑥:scope=global + placement=closed_only 被拒(global ⇒ any 无格子布置域)", _errs_has_cross_field(vt.validate()))
vt.rows.assign([{"variant_id": "g5", "display_name": "负戊", "unlock_level": 2, "scope": "global",
"effect": "resource_face", "effect_value": 51, "trigger": "always", "placement": "any",
"board_types": [], "counter_items": []}])
var vterr: Array = vt.validate()
var domain_hit := false
for e in vterr:
if String(e.get("category", "")) == "domain":
domain_hit = true
Kit.check("互锁负样本⑦:resource_face effect_value=51 越域 [0,50] 被拒(不适用 flat_damage [1,9] 加法核)", domain_hit)
# ② 门控第三维(节点身份——global 行仅 boss/elite 入池;缺省/普通节点静默跳过零报错)
var pool_boss_ids: Array = []
for r in _db.variant_pool(2, "underdark", "boss"):
pool_boss_ids.append(String(r.variant_id))
Kit.check("门控三维:L2 underdark boss 池 = region 两行(贫矿脉 2026-10-16 屏蔽删行后无 global 行)", pool_boss_ids == ["magma", "ash"])
var pool_battle_ids: Array = []
for r in _db.variant_pool(2, "underdark", "battle"):
pool_battle_ids.append(String(r.variant_id))
Kit.check("门控三维:battle 节点静默跳过 global 行(普通图零报错零警告)", pool_battle_ids == ["magma", "ash"])
Kit.check("门控三维:缺省双参调用 = global 行跳过(既有调用口径零扰动)", _db.variant_pool(2, "underdark").is_empty() == false and _db.variant_pool(2, "underdark").size() == 2)
Kit.check("L1 Boss 免疫:unlock_level=2 门控在 boss 节点同样生效(教学保护带 B)", _db.variant_pool(1, "underdark", "boss").is_empty())
Kit.check("全局决议:battle 节点恒无全局型", _db.global_variant_row(2, "underdark", "battle").is_empty())
Kit.check("全局决议:L2 boss 全局池空 → 空行(贫矿脉屏蔽后兜底回落承载)", _db.global_variant_row(2, "underdark", "boss").is_empty())
Kit.check("全局决议:L1 boss 全局池空 → 空行(兜底回落承载)", _db.global_variant_row(1, "underdark", "boss").is_empty() and _db.global_variant_row(1, "dungeon", "boss").is_empty())
Kit.check("elite 前向词位:boss 同池恒空(贫矿脉屏蔽后——词表位不造行)", _db.global_variant_row(2, "underdark", "elite").is_empty())
# ③ 兜底回落:全局池空 → Boss 图改走 region 候选池(恰一行 region 变异 + 格子级布置 + 正常取号)
var crng = _CountVariantRng.new()
crng.set_master_seed(42)
var boss_settlement = load("res://core/battle_orchestrator.gd").new(_db, crng)
var boss_ok: bool = boss_settlement.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"})
Kit.check("L2 Boss 战斗启动(node_depth=9 Boss 终点——兜底回落断言载体)", boss_ok)
var boss1_ids: Array = []
var boss1_variants: Dictionary = {}
var boss1_calls := 0
if boss_ok:
var bb = boss_settlement.minesweeper.current_board()
Kit.check("屏蔽后回落:Boss 图无整图全局型(board_variants 空——全局池空合法态)", bb.board_variant_ids().is_empty())
var boss_vids := {}
for vc in bb.variants:
for vid in bb.variants[vc]:
boss_vids[String(vid)] = true
Kit.check("屏蔽后回落:Boss 图 region 变异恰一行且在候选池内(实测 %s)" % str(boss_vids.keys()), boss_vids.size() == 1 and (boss_vids.has("magma") or boss_vids.has("ash")))
Kit.check("屏蔽后回落:Boss 图恢复 region 变异格布置(原替换制跳过 region 的逻辑随全局池空失效)", not bb.variants.is_empty())
Kit.check("屏蔽后回落:variant_place 恢复取号(原全局取用零取号态消失)", crng.variant_calls > 0)
var snap: Dictionary = boss_settlement.presentation_snapshot()
Kit.check("快照投影:board_variants 空态一致(横幅/角标唯一读取面——无全局型即零预告)", _arr_str_eq(snap.get("board_variants", []), bb.board_variant_ids()))
boss1_ids = bb.board_variant_ids()
boss1_variants = bb.variants.duplicate()
boss1_calls = crng.variant_calls
# Boss 图确定性:同种子两遍逐位相同
var crng2 = _CountVariantRng.new()
crng2.set_master_seed(42)
var boss2 = load("res://core/battle_orchestrator.gd").new(_db, crng2)
if boss2.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}):
var bb2 = boss2.minesweeper.current_board()
Kit.check("确定性:同种子 Boss 图两遍 board_variants/变异格/取号逐位相同(region 布置路径)", _arr_str_eq(bb2.board_variant_ids(), boss1_ids) and bb2.variants == boss1_variants and crng2.variant_calls == boss1_calls)
# ④ 资源面偏斜投影(RewardEconomy 纯函数面——守恒/白名单/构造例/合成序)
var eco = load("res://core/reward_economy.gd").new(_db, load("res://core/rng_service.gd").new())
var base_rows: Array = eco._rollable_rows()
var base_sum: int = eco._pool_weight(base_rows)
Kit.check("偏斜基线:正向池现行合计 77(挖掘风险批口径)", base_sum == 77)
eco.board_skew = 0
var off_rows: Array = eco._biased_cell_rows(0) # 内容数据驱动扩展批 T4:偏运权重改 reward_roll 订阅命中入参(luck_bias 注入位退役)
Kit.check("off 态零扰动:board_skew=0 行集与基线逐位相同(不进投影域)", off_rows.size() == base_rows.size() and _rows_weights_equal(off_rows, base_rows))
eco.board_skew = 30
var on_rows: Array = eco._biased_cell_rows(0)
Kit.check("合计守恒:偏斜后正向池合计 = 77(整数最大余数重归一)", eco._pool_weight(on_rows) == 77)
var wmap := _rows_weight_map(on_rows)
Kit.check("白名单豁免:weaken/gp/action 三行偏斜后逐位不动", wmap.get("weaken", -1) == 8 and wmap.get("ap_up", -1) == 6 and wmap.get("gp_coin", -1) == 2)
Kit.check("生存族下调:heal 4→3 / defense 11→7(v=30 构造例)", wmap.get("heal_minor", -1) == 3 and wmap.get("ac_up", -1) == 7)
Kit.check("进攻族上浮:attack 14→15 / blast 16→18 / pierce 8→9 / magic 8→9(v=30 构造例,等比上浮守恒)", wmap.get("attack_up", -1) == 15 and wmap.get("spark_aoe", -1) == 18 and wmap.get("pierce_strike", -1) == 9 and wmap.get("magic_bolt", -1) == 9)
eco.board_skew = 30
var combo_rows: Array = eco._biased_cell_rows(2) # 件级加算 2=偏运硬币行 value(reward_roll bias_rewards 命中面同值)
var cw := _rows_weight_map(combo_rows)
Kit.check("合成序:图级偏斜先(乘法面)件级 luck_bias 后(加法 +2×四稀有类)——合计 77+8", eco._pool_weight(combo_rows) == 77 + 4 * 2)
Kit.check("合成序构造例:skew(heal 3)+2=5 / gp 2+2=4 / pierce 9+2=11 / magic 9+2=11(图级定型后玩家构筑叠加)", cw.get("heal_minor", -1) == 5 and cw.get("gp_coin", -1) == 4 and cw.get("pierce_strike", -1) == 11 and cw.get("magic_bolt", -1) == 11)
eco.board_skew = 50
var v50: Dictionary = _rows_weight_map(eco._biased_cell_rows(0))
Kit.check("域上界 v=50:守恒成立且逐行权重恒正(域 [0,50] 安全)", eco._pool_weight(eco._biased_cell_rows(0)) == 77 and int(v50.get("heal_minor", -1)) > 0)
# 普通图摇号全链路确定性(off 路径)——同种子普通图资源映射逐位相同(golden 对拍另有批前快照)
var nrng = load("res://core/rng_service.gd").new()
nrng.set_master_seed(42)
var normal1 = load("res://core/battle_orchestrator.gd").new(_db, nrng)
normal1.begin_battle({"level_index": 2, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)})
var n1: Dictionary = _board_resource_dump(normal1)
var nrng2 = load("res://core/rng_service.gd").new()
nrng2.set_master_seed(42)
var normal2 = load("res://core/battle_orchestrator.gd").new(_db, nrng2)
normal2.begin_battle({"level_index": 2, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)})
Kit.check("确定性:同种子普通图布置/摇号两遍逐位相同(off 全链路)", n1 == _board_resource_dump(normal2) and not n1.is_empty())
func _arr_str_eq(a, b: Array) -> bool:
var arr: Array = a if a is Array else []
if arr.size() != b.size():
return false
for i in arr.size():
if String(arr[i]) != String(b[i]):
return false
return true
func _rows_weights_equal(a: Array, b: Array) -> bool:
if a.size() != b.size():
return false
for i in a.size():
if String(a[i].get("reward_id", "")) != String(b[i].get("reward_id", "")) or int(a[i].weight) != int(b[i].weight):
return false
return true
func _rows_weight_map(rows: Array) -> Dictionary:
var m := {}
for r in rows:
m[String(r.get("reward_id", ""))] = int(r.weight)
return m
func _board_resource_dump(settlement) -> Dictionary:
var b = settlement.minesweeper.current_board()
var dump := {}
for c in b.variants.keys():
dump["v:%d,%d" % [c.x, c.y]] = ",".join(b.variants[c])
for y in b.height:
for x in b.width:
var ident = settlement._reward_economy.query_identity(Vector2i(x, y))
if ident != null:
dump["r:%d,%d" % [x, y]] = String(ident.reward_id)
return dump
# ---- 地形变异 布置段(棋盘设置顺序第 4 步断言面)----
# 布置发生在编排器战斗初始化(install 初始展开 + roll_board 资源格之后),生成器产物零变异——
# 本段经 begin_battle 走完整初始化序后断言:门控一致性 / 布置域 / 数量公式 / 连通片上限 / 确定性。
func _errs_has_cross_field(errs: Array) -> bool:
for e in errs:
if String(e.get("category", "")) == "cross_field":
return true
return false
# ---- 全局作用域变异 断言面(「全局池空 → region 兜底回落」路径
# ——互锁负样本/门控三维/资源面偏斜守恒与合成序/快照投影不变)----
# 取号计数桩(variant_place 取号断言——子类拦截计数,派生与取数语义零改动)
@@ -0,0 +1,77 @@
# tests/unit/contracts/source_guard_test.gd — 仓库自身源码契约单元测试宿主
# 断言面 = 两套(批 B 断言迁移自 smoke_all.gd,G12 施工组;被测对象=仓库自身源文件,
# 主会话裁决五新设 source_guard 子目录归「契约」语义):
# 类型闸(project.godot warning 配置在位 + 审计器闸机接线)
# 卫生件 grep 闸(产品码字符串 Callable/死代码/GP 影子账残留零命中 + 层序落位单口)
# 协议照单元侧惯例:extends SceneTree+Kit.check 主通道;被测面是仓库文件直读
# (FileAccess/DirAccess/project.godot 设置),零表夹具零 RNG。
# 运行:godot --headless --path game --script res://tests/unit/contracts/source_guard_test.gd
extends SceneTree
const Kit := preload("res://tests/unit/test_kit.gd")
func _init() -> void:
# 套执行序沿源编排序(_split_tail 尾段调用序)
_smoke_type_gate()
_smoke_hygiene_gates()
quit(Kit.finish("source_guard_test"))
# ---- 类型闸断言(V7——project.godot warning 配置在位〔源码断言〕+审计器脚本在位;
# 审计器归零判定走 static_scan S9 闸机,此处兜配置面)----
func _smoke_type_gate() -> void:
var pj: String = FileAccess.get_file_as_string("res://project.godot")
Kit.check("V7 类型闸 warning 配置在位(untyped_declaration=1 显形)〔迭代27 T5〕",
pj.contains("gdscript/warnings/untyped_declaration=1"))
var scan_src: String = FileAccess.get_file_as_string("res://tests/static_scan.sh")
Kit.check("V7 类型审计器闸机在位(static_scan S9 → tools/audit_type_hints.py)〔迭代27 T5〕",
scan_src.contains("audit_type_hints.py"))
# ---- 卫生件 grep 闸(V8——死代码/字符串 Callable/影子账残留零命中;源码扫描面)----
func _smoke_hygiene_gates() -> void:
var callables := 0
for d in ["res://core", "res://presentation", "res://data"]:
var dir := DirAccess.open(d)
if dir == null:
continue
dir.list_dir_begin()
var f := dir.get_next()
while f != "":
if dir.current_is_dir() and not f.begins_with("."):
var sub := DirAccess.open(d + "/" + f)
if sub != null:
sub.list_dir_begin()
var g := sub.get_next()
while g != "":
if g.ends_with(".gd"):
var t: String = FileAccess.get_file_as_string(d + "/" + f + "/" + g)
callables += t.count("Callable(self, \"")
g = sub.get_next()
elif f.ends_with(".gd"):
var t2: String = FileAccess.get_file_as_string(d + "/" + f)
callables += t2.count("Callable(self, \"")
f = dir.get_next()
Kit.check("V8 字符串 Callable 产品码零命中(实测应 0——方法引用换装闸)〔迭代27 T6〕",
callables == 0)
Kit.check("V8 FeatsEngine 整文件退役(class_name 注销=文件删除)〔迭代27 T6〕",
not FileAccess.file_exists("res://core/feats_engine.gd"))
var bo: String = FileAccess.get_file_as_string("res://core/battle_orchestrator.gd")
Kit.check("V8 死代码抽查·_attune_row 零残留〔迭代27 T6〕",
not bo.contains("func _attune_row"))
var sm: String = FileAccess.get_file_as_string("res://core/save_manager.gd")
Kit.check("V8 死代码抽查·attunement_counters 零残留〔迭代27 T6〕",
not sm.contains("func attunement_counters"))
var mv: String = FileAccess.get_file_as_string("res://presentation/main.gd")
Kit.check("V8 GP 影子账残留零命中(main 源码面——文件已删+委托缝已拆)〔迭代27 T3〕",
not mv.contains("gp_shadow_ledger") and not mv.contains("_gp_ledger"))
# V1d 层序落位单口:main.gd 的 move_child 调用全部收在 _apply_layer_contract 内(合同装配单口——
# 函数体之外零调用点;出现第二处落位=硬编码层序,须改合同表而非加调用)
var fn_start: int = mv.find("func _apply_layer_contract")
var fn_end: int = mv.find("\nfunc ", fn_start + 10)
if fn_end < 0:
fn_end = mv.length()
var outside_calls: int = (mv.substr(0, fn_start) + mv.substr(fn_end)).count("move_child(")
Kit.check("V1d 层序落位单口(_apply_layer_contract 之外 move_child 调用点=%d 应=0)〔装配生命批 V1〕" % outside_calls,
outside_calls == 0)
@@ -0,0 +1,820 @@
# tests/unit/core/adventure_feats_test.gd — 冒险结构与专长单元测试宿主
# 断言面 = 三套(批 B 断言迁移自 smoke_all.gd,G7 施工组):
# 冒险结构 API 闸(状态机受理面——重复开始拒绝/入口停点/放弃两态)
# 冒险验收断言(三关序列/大奖励三层兑付/账本封闭/败北线/恢复数学/加算区同账/共享池)
# 专长重设计实装(feat_table 四级/案D 池接线/V3 订阅面逐件三列/专长凭证保底)
# 注:断点往返三条(基点 1420–1426)按主会话裁决②改判集成宿主,不在本宿主断言面内。
# 协议照单元侧惯例:extends SceneTree+Kit.check 主通道;--script 模式无 autoload,
# ConfigDB 显式实例化构造注入(_fresh_rng 同种子隔离夹具——RngService 构造注入 runner);
# 表加载失败不中止(沿被迁断言面的源语义——承 G2–G6 同款)。
# 运行:godot --headless --path game --script res://tests/unit/core/adventure_feats_test.gd
extends SceneTree
const Kit := preload("res://tests/unit/test_kit.gd")
const ConfigDBScript := preload("res://core/config_db.gd")
var _db
func _init() -> void:
_db = ConfigDBScript.new()
# 表加载失败不中止(承 G2/G3/G4/G5 同款:表损坏的红落在具体断言上——
# 诊断在此展开,后续断言按坏表实况逐条验红;collect_only=true 下成员照常赋值可评)
var load_ok: bool = _db.load_tables("res://data/tables", true)
if not load_ok:
push_error("[adventure_feats_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])
# 套执行序沿源编排(基点 _run_core_smoke 调用序——本组套的相对序)
_smoke_adventure_api()
_smoke_adventure()
_smoke_feats()
quit(Kit.finish("adventure_feats_test"))
# ---- 冒险结构冒烟(AdventureTable 建表 + AdventureRunner + 冒险验收断言)----
# 令牌候选匹配(TP0——kind+tier 复合身份;冒烟断言面共用)
func _smoke_find_token(candidates: Array, kind: String, tier: String) -> Dictionary:
for t in candidates:
var tok: Dictionary = t as Dictionary
if String(tok.get("kind", "")) == kind and String(tok.get("tier", "")) == tier:
return tok
return {}
# ② 冒险结构 API 闸(状态机受理面——重复开始拒绝 / 入口停点 / 放弃两态)
# ② 冒险结构 API 闸(状态机受理面——重复开始拒绝 / 入口停点 / 放弃两态)
func _smoke_adventure_api() -> void:
print("--- 冒险结构 API 闸(重复开始拒绝 · 入口停点 · 放弃两态受理)---")
var adr = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(42))
adr.set_hero_override(900, 400) # 首战速胜档(放弃的非战斗受理要走到选路态)
# 入口停点观测(信号序 + 入口候选快照——容器写回规避 lambda 按值捕获)
var sig_seq: Array = []
var obs := {"entry_cands": []}
adr.level_started.connect(_api_mark_started.bind(sig_seq))
adr.choices_presented.connect(_api_mark_choices.bind(sig_seq, obs))
adr.node_entered.connect(_api_mark_node.bind(sig_seq))
Kit.check("冒险启动受理(主种子注入恰一次——state=running;first_script=固定模板 0——精确路径断言需要现行图,零抽签)", adr.start_adventure(42, {"first_script": true}))
Kit.check("进行中重复 start_adventure 拒绝(重开须待结算完毕)", not adr.start_adventure(43))
Kit.check("停点前移次序契约:level_started 先于入口候选呈现(反序选路口会被呈现层 runner 空防御丢弃)", sig_seq.size() == 2 and String(sig_seq[0]) == "level_started" and String(sig_seq[1]) == "choices_presented")
Kit.check("入口停点:选路态 + 入口单候选 + 首战未开(呈现零 RNG 消耗——pick 前零战斗域)",
adr.runner().awaiting_choice() and (obs["entry_cands"] as Array).size() == 1 \
and adr.runner().current_battle() == null)
Kit.check("pick 入口候选受理(同一 _enter_node——与旧自动进入逐位等价)", adr.runner().pick_next_node(int(adr.runner().next_candidate_ids()[0])))
Kit.check("pick 后 node_entered 到达 + 首战在飞(接口①开始关卡——停点闭合直达战斗节点)", sig_seq.size() == 3 and String(sig_seq[2]) == "node_entered" and adr.runner().current_battle() != null)
Kit.check("战斗中 abandon 拒绝(战斗域单事务在飞)", not adr.abandon())
# 驱动首战到胜利 → 选路态(非战斗期)→ abandon 受理
var autoplay = load("res://smoke/level_autoplay.gd")
var battle = adr.runner().current_battle()
var st := _battle_st("normal")
var guard := 30
while adr.runner().current_battle() == battle and guard > 0:
guard -= 1
autoplay._autoplay_battle(adr.runner(), battle, st, null)
Kit.check("首战胜后选路态(非战斗期——current_battle 置空)", adr.runner().current_battle() == null and adr.runner().awaiting_choice())
Kit.check("非战斗期 abandon 受理(选路态——同失败型结算)", adr.abandon())
var sum: Dictionary = adr.summary
Kit.check("放弃结算:result=abandoned · cleared=false(已获面不追回——钥石累计 %d)" % int(sum.get("keystone_total", -1)), String(sum.get("result", "")) == "abandoned" and not bool(sum.get("cleared", true)))
Kit.check("结算后重复 abandon 拒绝(结算受理只算一次——state=idle)", not adr.abandon())
# 观测桩(信号序采集——bind 容器写回,签名按各信号实参定制)
# 观测桩(信号序采集——bind 容器写回,签名按各信号实参定制)
func _api_mark_started(_li: int, _dn: String, seq: Array) -> void:
seq.append("level_started")
func _api_mark_choices(cands: Array, seq: Array, obs: Dictionary) -> void:
seq.append("choices_presented")
obs["entry_cands"] = cands.duplicate()
func _api_mark_node(_node: Dictionary, _info: Dictionary, seq: Array) -> void:
seq.append("node_entered")
# ③ 冒险验收断言(映射 冒险结构.md 验收标准——表覆盖/满配通关/种子复现/大奖励/败北线/
# 重复拒绝/GP 跨关延续/加算区同账/恢复数学/放弃)
# ③ 冒险验收断言(映射 冒险结构.md 验收标准——表覆盖/满配通关/种子复现/大奖励/败北线/
# 重复拒绝/GP 跨关延续/加算区同账/恢复数学/放弃)
func _smoke_adventure() -> void:
print("--- 冒险结构验收(三关序列 · 大奖励 · 摘要五件事 · 账本封闭)---")
var autoplay = load("res://smoke/level_autoplay.gd")
# ① 表与覆盖义务(序列关数 = 行集关数;行集内每个战斗/Boss 节点跨表精确行集——覆盖义务断言;
# enemy 侧键域 = (level_index, row_kind, depth_band)——战斗节点查 battle 键域、Boss 节点查 boss 键域,
# 同带多行合法,带键聚合天然兼容〔fv24〕)
Kit.check("序列关数 = 行集关数(三关正式序列生效)", int(_db.level.rows.size()) == 3)
var bg_keys := {}
for row in _db.board_gen.rows:
bg_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true
var en_keys := {}
for row in _db.enemy.rows:
en_keys["%d_%s_%d" % [int(row.level_index), String(row.get("row_kind", "")), int(row.get("depth_band", -1))]] = true
var miss := 0
for row in _db.level.rows:
for n in _level_active_nodes(int(row.level_index)):
var nt := String(n.get("type", ""))
if nt != "battle" and nt != "boss":
continue
var key := "%d_%s_%d" % [int(row.level_index), nt, int(n.get("node_depth", -1))]
if not bg_keys.has("%d_%d" % [int(row.level_index), int(n.get("node_depth", -1))]) or not en_keys.has(key):
miss += 1
Kit.check("行集内每个战斗/Boss 节点在 board_gen + enemy 有精确行集(缺失 %d——带键 (L,row_kind,depth_band))" % miss, miss == 0)
# ② 满配通关 + ④ 三层奖励兑付(三选一恰 2 次 · 回血封顶 · 钥石逐杀 · 候选零重复)
var st: Dictionary = autoplay.run_adventure(_db, 42, 900, 400)
var sum: Dictionary = st["summary"]
Kit.check("满配通关(42 / atk+900 / hp+400——cleared=true;v2 怪阶梯后旧 200/100 档不再满配)", String(st["result"]) == "completed" and bool(sum.get("cleared", false)))
Kit.check("通关几关 = 3(levels_cleared)", int(sum.get("levels_cleared", -1)) == 3)
var ks_exp := 0
for li in range(1, int(_db.level.rows.size()) + 1):
ks_exp += int(_db.meta_boss_keystone_row(li).get("first_kill", 0))
Kit.check("钥石合计 = ΣBoss 首杀行值(%d——逐杀掉落新口径;断点未注入恒首杀)" % int(sum.get("keystone_total", -1)), int(sum.get("keystone_total", -1)) == ks_exp)
var drops: Array = sum.get("keystone_drops", []) as Array
Kit.check("逐杀掉落明细恰 3 笔(L1/L2/L3——additive 摘要键 keystone_drops)", drops.size() == 3)
Kit.check("摘要携带主种子(42)", int(sum.get("master_seed", -1)) == 42)
var picks: Array = []
var resolves: Array = []
var node_picks := 0
for ev in (st["reward_events"] as Array):
if (ev as Dictionary).has("curio_pick"):
picks.append((ev as Dictionary)["curio_pick"])
elif (ev as Dictionary).has("node_curio_pick"):
node_picks += 1 # 奖励节点三选一(每关奖励节点各 1 次)
elif (ev as Dictionary).has("curio_resolved"):
resolves.append((ev as Dictionary)["curio_resolved"])
Kit.check("通关三选一恰呈现 2 次、恰兑付 2 次(L1/L2 后——末关不发;resolves 含节点回执另计)", picks.size() == 2 and resolves.size() == 2 + node_picks)
Kit.check("奖励节点三选一 ≥1 次(L1 链必经 1 个;L2/L3 图可选路经若干——实际经停 %d)" % node_picks, node_picks >= 1)
var node_resolves := 0
for r0 in resolves:
if String((r0 as Dictionary).get("context", "")) == "reward_node":
node_resolves += 1
Kit.check("奖励节点兑付回执与经停逐笔相等(context=reward_node——同账计数 %d/%d)" % [node_resolves, node_picks], node_resolves == node_picks)
var level_resolves: Array = []
for r0 in resolves:
if String((r0 as Dictionary).get("context", "")) != "reward_node":
level_resolves.append(r0)
var heal_ok := picks.size() == 2
var cand_ok := picks.size() == 2
var ks_gain_ok := picks.size() == 2
var resolve_ok := level_resolves.size() == 2
var max_flat := int(_db.adventure.curio_value_max_hp_common)
var heal_pct := int(_db.adventure.clear_heal_pct)
var hp_acc := 0 # max_hp 件累加(回血封顶断言的生效上限推导——TP0 起按兑付令牌实值,大小档分别计)
var ks_prefix := 0
for i in picks.size():
var p: Dictionary = picks[i]
var cands: Array = p.get("candidates", []) as Array
if cands.size() < 2 or cands.size() > 3:
cand_ok = false
var seen := {}
for c in cands:
# 身份键 = kind|tier|feat_id(TP0 v2.2 relic 按 feat_id 分列
# 不参与同 kind 去重〔_token_identity relic: 前缀〕,同 tier 三枚不同流派件=三个不同选项合法态)
var ckey := "%s|%s|%s" % [String((c as Dictionary).get("kind", "")), String((c as Dictionary).get("tier", "")), String((c as Dictionary).get("feat_id", ""))]
if seen.has(ckey):
cand_ok = false
seen[ckey] = true
var heal: int = int(p.get("heal_gained", -1))
var hp_before: int = int(p.get("hp_before", -1))
var hp_after: int = int(p.get("hp_after", -1))
# 账本生效上限口径:置满/回血封顶按生效上限(flat 后乘调试浮窗 pct)——本段 pct=400
# 加算区 = 树/锻造件(hp_acc)+购血观测值(level_autoplay 停点快照 bonus_max_hp)——精确断言不再窗口拟合
var extra_obs := int(p.get("bonus_max_hp", -1))
var m_obs := int(round(float(int(_db.hero.max_hp) + hp_acc + extra_obs) * 5.0))
var heal_exp := mini(int(floor(float(m_obs) * float(heal_pct) / 100.0)), m_obs - hp_before)
if extra_obs < 0 or heal != heal_exp or hp_after != hp_before + heal or hp_after > m_obs:
heal_ok = false
ks_prefix += int(_db.meta_boss_keystone_row(int(p.get("level_index", -1))).get("first_kill", 0))
if int(p.get("keystone_gained", -1)) != int(_db.meta_boss_keystone_row(int(p.get("level_index", -1))).get("first_kill", 0)) or int(p.get("keystone_total", -1)) != ks_prefix:
ks_gain_ok = false
if resolve_ok:
var r: Dictionary = level_resolves[i]
var rk := String(r.get("picked", ""))
var rt := String(r.get("tier", ""))
if _smoke_find_token(p.get("candidates", []) as Array, rk, rt).is_empty() or int(r.get("value", -1)) != int(_db.adventure.curio_value(rk, rt)):
resolve_ok = false
if rk == "max_hp":
hp_acc += int(r.get("value", max_flat))
Kit.check("三选一候选 2–3 枚且零重复(令牌洗牌不放回——kind+tier 复合身份)", cand_ok)
Kit.check("通关部分回血封顶(floor(观测上限×比例) 封顶——购血观测值精确断言,核代码增补)", heal_ok)
Kit.check("钥石逐杀入账(首杀档位 + 累计逐位)", ks_gain_ok)
Kit.check("点选恰兑付选中一件(值=令牌档位表值;未选项零生效)", resolve_ok)
var seq: Array = st["encounter_max_hp_seq"]
var k := 5.0 # hp+400 ⇒ 调试浮窗百分比后乘 ×5(加算先加、百分比后乘——生效次序口径)
var exp_base := maxi(1, int(round(float(int(_db.hero.max_hp)) * k)))
# 模拟档 v3.4 商店贪心购血合法抬升——上限窗口 = 基础 + 加算区 3 件 + 购血余量;
# 大生命 +10(优秀档)——上界按大件最坏构成 3×10+购血 15=+45(TP0 放宽承袭)
var hp_ceiling := maxi(1, int(round(float(int(_db.hero.max_hp) + 3 * int(_db.adventure.curio_value("max_hp", "excellent")) + 15) * k)))
var seq_ok := seq.size() > 0 and int(seq[0]) == exp_base
for v in (seq as Array):
if int(v) < exp_base or int(v) > hp_ceiling:
seq_ok = false
Kit.check("各战上限入参 ∈ [基础, 基础+3×件值+购血余量](覆盖值通道——双渠道共享上限 3;模拟档贪心购血合法叠加〔数值修复批 v3.4〕;首战=基础 %d)" % exp_base, seq_ok)
# ③ 种子域可复现(同种子两局:summary + 上限序列 + 行进序列 + 三选一停点载荷逐位一致)
var st2: Dictionary = autoplay.run_adventure(_db, 42, 900, 400)
Kit.check("同种子两局逐位可复现(summary/上限序列/行进序列/三选一载荷——reward_pick 封闭流)",
st2["summary"] == sum and st2["encounter_max_hp_seq"] == seq
and st2["visited_ids"] == st["visited_ids"] and st2["reward_events"] == st["reward_events"])
# ⑤ 败北线(零配 L1 败北 + 半配 L2 败北——种子扫描,仿商店房 range(42,92) 先例)
var lose := {}
for s in range(42, 92):
var stl: Dictionary = autoplay.run_adventure(_db, s, 0, 0)
if String(stl["result"]) == "failed" and int(stl["summary"].get("stopped_at", -1)) == 1:
lose = {"s": s, "st": stl}
break
Kit.check("零配 L1 败北种子可达(seed=%s)" % str(lose.get("s", -1)), not lose.is_empty())
if not lose.is_empty():
var sl: Dictionary = lose["st"]["summary"]
Kit.check("L1 败北摘要:未打穿 · 通关 0 关 · 钥石 0 · 止步第 1 层",
not bool(sl["cleared"]) and int(sl["levels_cleared"]) == 0 \
and int(sl["keystone_total"]) == 0 and int(sl["stopped_at"]) == 1)
var dl: Dictionary = sl.get("defeat", {})
Kit.check("L1 败北败退明细:层号/节点编号/节点类型在案(数值验证取数面 2026-09-22)",
int(dl.get("level_index", -1)) == 1 and int(dl.get("node_id", -1)) >= 0 \
and String(dl.get("node_type", "")) in ["battle", "boss"])
var half := {}
for s in range(42, 142):
var sth: Dictionary = autoplay.run_adventure(_db, s, 50, 0)
if String(sth["result"]) == "failed" and int(sth["summary"].get("stopped_at", -1)) == 2:
half = {"s": s, "st": sth}
break
Kit.check("半配(atk+50)L2 败北种子可达(seed=%s)" % str(half.get("s", -1)), not half.is_empty())
if not half.is_empty():
var sh: Dictionary = half["st"]["summary"]
Kit.check("L2 败北摘要:L1 已过(钥石=L1 首杀掉落)· 止步第 2 层",
not bool(sh["cleared"]) and int(sh["levels_cleared"]) == 1 \
and int(sh["keystone_total"]) == int(_db.meta_boss_keystone_row(1).get("first_kill", 0)) \
and int(sh["stopped_at"]) == 2)
var dh: Dictionary = sh.get("defeat", {})
Kit.check("L2 败北败退明细:层号=2 · 节点在案(数值验证取数面 2026-09-22)",
int(dh.get("level_index", -1)) == 2 and int(dh.get("node_id", -1)) >= 0 \
and String(dh.get("node_type", "")) in ["battle", "boss"])
# ⑥ 重复宣布拒绝(settle 后再调 _on_level_finished——摘要零变化)
var adr6 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(7))
adr6.set_hero_override(900, 400)
Kit.check("重复拒绝场景:冒险启动(seed 7——固定模板 0 同上)", adr6.start_adventure(7, {"first_script": true}))
adr6.runner().pick_next_node(int(adr6.runner().next_candidate_ids()[0])) # 入口停点 pick 进首战
var b6 = adr6.runner().current_battle()
var st6 := _adv_st()
var g6 := 30
while adr6.runner().current_battle() == b6 and g6 > 0:
g6 -= 1
autoplay._autoplay_battle(adr6.runner(), b6, st6, null)
Kit.check("重复拒绝场景就位:选路态放弃 → 已结算", adr6.abandon())
var sum6_before: Dictionary = adr6.summary.duplicate(true)
adr6._on_level_finished({"result": "victory", "hp": 10, "node_index": 1, "node_id": 1, "node_type": "battle"})
Kit.check("重复关卡结束宣布拒绝(账本与摘要零变化——契约 7)", adr6.summary == sum6_before)
# ⑦ GP 跨关延续(L1 收束无 clear · 每战入账恰一次 · L2 商店房余额 > 0 · 冒险收束 clear 恰一次为末事件)
var wallet: Array = st["wallet_events"]
var clears := 0
var gains := 0
for ev in wallet:
if String(ev["event"]) == "clear":
clears += 1
elif String(ev["event"]) == "gain" and String(ev.get("source", "")) != "curio_pick":
gains += 1 # 战斗完成入账计数(三选一 GP 袋件入账另有 source 标记——不计战斗口径)
Kit.check("每战完成入账恰一次(gain %d = battles %d——GP 袋件入账单列)" % [gains, int(st["battles"])], gains == int(st["battles"]))
Kit.check("钱包 clear 恰一次且为末事件(关卡收束不清——GP 冒险收束清零归冒险结构)", clears == 1 and wallet.size() > 0 and String(wallet[wallet.size() - 1]["event"]) == "clear")
var shop_seq: Array = st["shop_gp_seq"]
Kit.check("L2 商店房到达余额 > 0(%d——L1 积累跨关延续;两关商店房必经各 1)" % (shop_seq[0] if shop_seq.size() > 0 else -1), shop_seq.size() == 2 and int(shop_seq[0]) > 0)
# ⑧ 加算区同账(手工场景:L2 商店购买 → 三选一直调——同一本账累加 + 部分回血封顶)
var same := {}
for s in range(42, 92):
var a8 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(s))
a8.set_hero_override(900, 400)
if not a8.start_adventure(s, {"first_script": true}): # 固定模板 0——种子扫描语义按现行图
continue
if _adv_drive_to_shop(a8, _adv_st(), autoplay):
same = {"a": a8, "s": s}
break
Kit.check("同账场景种子可达(seed=%s——L2 商店房就位)" % str(same.get("s", -1)), not same.is_empty())
if not same.is_empty():
var a8 = same["a"]
var r8 = a8.runner()
var gp0: int = int(r8.gp)
var hp_price := -1
for p in _db.shop.products:
if String(p.get("product_id", "")) == "hp_up":
hp_price = int(p.get("base_price", -1))
var l2_price_bonus := 0
for pb in _db.shop.price_level_bonus:
if int(pb.get("level_index", -1)) == 2:
l2_price_bonus = int(pb.get("bonus", 0))
Kit.check("进店余额 ≥ hp_up L2 首价(%d = 基价 %d + 关卡加成 %d——配置推导)" % [hp_price + l2_price_bonus, hp_price, l2_price_bonus], gp0 >= hp_price + l2_price_bonus)
Kit.check("购买 hp_up 受理(出账 + 加算区)", r8.buy_product("hp_up"))
var acc_shop := int(a8.run_ledger.bonus.get("max_hp", -1)) # L1 三选一后 + 商店 3(首件随种子)
Kit.check("统一冒险加算区含商店值(max_hp=%d ≥ 商店 3——L1 三选一件值另计)" % acc_shop, acc_shop >= 3)
a8._on_level_finished({"result": "victory", "hp": int(r8.hero_hp), "node_index": r8.node_index,
"node_id": r8.current_node_id(), "node_type": "shop"}) # 模拟 L2 通关(victory 载荷直调——非末关)
Kit.check("L2 通关三选一停点在案(直调兑付前——零自动推进)", not a8._curio_pending.is_empty())
Kit.check("未兑付推进拒绝(选择停点零自动推进契约)", not a8.continue_to_next_level())
var l2_cands: Array = a8._curio_pending["candidates"]
var l2_pick: Dictionary = l2_cands[0]
Kit.check("三选一点选兑付受理(候选取首件)", a8.pick_curio(String(l2_pick.get("kind", "")), String(l2_pick.get("tier", "")), String(l2_pick.get("feat_id", ""))))
var picked2: String = String(a8._curio_result.get("picked", ""))
var expect_acc: int = acc_shop + (int(a8._curio_result.get("value", 0)) if picked2 == "max_hp" else 0)
Kit.check("三选一属性件与商店购买同账累加(max_hp=%d = %d + 三选一%s)" % [int(a8.run_ledger.bonus.get("max_hp", -1)), acc_shop, picked2], int(a8.run_ledger.bonus.get("max_hp", -1)) == expect_acc)
var max_eff8: int = a8.effective_max_hp() # 账本生效上限含调试浮窗 pct(a8 已 set 400)
Kit.check("通关部分回血封顶(hp %d ≤ 生效上限 %d——血回满语义退役)" % [int(a8.ledger["hero_hp"]), max_eff8], int(a8.ledger["hero_hp"]) <= max_eff8)
Kit.check("兑付后推进受理(下一关开始)", a8.continue_to_next_level())
# ⑨ 恢复点数学(接口③封顶公式三读数 + 同式两落点一致)
var a9 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(99))
Kit.check("恢复点数学:floor(100×30%)=30 · 封顶缺口 5 · 满血回 0 合法",
int(a9.apply_rest(30, 70, 100)) == 30 and int(a9.apply_rest(30, 95, 100)) == 5 \
and int(a9.apply_rest(30, 100, 100)) == 0)
var lr9 = load("res://core/level_runner.gd").new(_db, _fresh_rng(99))
Kit.check("同一公式两个落点逐位一致(LevelRunner.rest_heal_amount 内置默认)", int(lr9.rest_heal_amount(30, 70, 100)) == 30 and int(lr9.rest_heal_amount(30, 95, 100)) == 5)
# ⑩ 放弃(战斗中拒绝 · 非战斗期受理同失败型结算)
var a10 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(15))
a10.set_hero_override(900, 400)
Kit.check("放弃场景:冒险启动(seed 15——固定模板 0 同上)", a10.start_adventure(15, {"first_script": true}))
Kit.check("放弃场景:入口停点 pick 进首战(停点前移批 2026-09-23 驱动口径)", a10.runner().pick_next_node(int(a10.runner().next_candidate_ids()[0])))
Kit.check("战斗中 abandon 拒绝(战斗域单事务在飞)", not a10.abandon())
var b10 = a10.runner().current_battle()
var st10 := _adv_st()
var g10 := 30
while a10.runner().current_battle() == b10 and g10 > 0:
g10 -= 1
autoplay._autoplay_battle(a10.runner(), b10, st10, null)
Kit.check("非战斗期(选路态)abandon 受理", a10.runner().current_battle() == null and a10.abandon())
# ⑪ 池耗即枯竭 + 池收缩 + 满血回 0(TP0——white-box:耗尽 attack/ap 令牌后候选零出现;
# 无种类次数上限,「达上限剔除」语义由牌库耗尽承载)
var a11 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(11))
Kit.check("剔牌库/替换场景:冒险启动(seed 11——道具解锁集只含 wide_scout_band)", a11.start_adventure(11, {"first_script": true, "treasure_unlocked": {"wide_scout_band": true}}))
for di in range(a11._curio_deck.size() - 1, -1, -1):
var dk := String((a11._curio_deck[di] as Dictionary).get("kind", ""))
if dk == "attack" or dk == "ap":
a11._curio_deck.remove_at(di)
var draw: Array = a11._draw_curio_candidates()
var draw_kinds := {}
for t in draw:
draw_kinds[String((t as Dictionary).get("kind", ""))] = true
Kit.check("池耗即枯竭(attack/ap 令牌耗尽不出候选——%s)" % str(draw), not draw_kinds.has("attack") and not draw_kinds.has("ap"))
Kit.check("池耗后候选恒 ≥2(稀有/罕见档余量——防「三选项全不可选」死角)", (draw as Array).size() >= 2)
# 池收缩(aux 不入三选一牌库:重复抽样零 aux 候选;同调位替换两路
# white-box 测试属停点渠道,零投放零行为)
var treasure_seen := false
for i in 40:
for t in a11._draw_curio_candidates():
if String((t as Dictionary).get("kind", "")) == "treasure_item":
treasure_seen = true
break
Kit.check("池收缩——40 次重抽零 aux 候选(停点渠道 aux 卡退役,迭代 11 第①批 T7)", not treasure_seen)
# 满血回 0 合法(通关回血封顶口径)
var a12 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(12))
a12.start_adventure(12, {"first_script": true})
var cap := {"payload": {}}
a12.curio_choice_presented.connect(func(p): cap["payload"] = p)
var max_full := int(_db.hero.max_hp) + int(a12._base_override.get("max_hp", 0))
a12.ledger["hero_hp"] = max_full
a12._on_level_finished({"result": "victory", "hp": max_full, "node_index": 1, "node_id": 1, "node_type": "shop"})
Kit.check("满血通关回血 0 合法(封顶口径——heal_gained=0 非负溢出)", int(cap["payload"].get("heal_gained", -1)) == 0)
# ⑬ 奖励节点共享池(同库同池同摘牌;TP0:attack 令牌耗尽后节点域同样零出现)
var a13 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(13))
Kit.check("节点共享池场景:冒险启动(seed 13)", a13.start_adventure(13, {"first_script": true, "treasure_unlocked": {"wide_scout_band": true}}))
for di13 in range(a13._curio_deck.size() - 1, -1, -1):
if String((a13._curio_deck[di13] as Dictionary).get("kind", "")) == "attack":
a13._curio_deck.remove_at(di13)
var ndraw: Dictionary = a13.node_curio_candidates()
var ncands13: Array = ndraw.get("candidates", []) as Array
var has_attack13 := false
for t in ncands13:
if String((t as Dictionary).get("kind", "")) == "attack":
has_attack13 = true
break
Kit.check("奖励节点共享池:令牌耗尽同样不出候选(同库同池——%s)" % str(ncands13), not has_attack13)
a13.present_node_curio(ncands13)
a13.present_node_curio([{"kind": "gp", "tier": "common"}])
var gp13 := int(a13.ledger.get("gp", 0))
Kit.check("奖励节点 GP 件入账(钱包「奖励入账」收口——+%d)" % int(_db.adventure.curio_value("gp", "common")), a13.apply_node_curio("gp", "common") and int(a13.ledger.get("gp", 0)) == gp13 + int(_db.adventure.curio_value("gp", "common")))
Kit.check("节点停点态兑付后清零(恰一次语义同构)", a13._node_curio_pending.is_empty())
var s10: Dictionary = a10.summary
Kit.check("放弃摘要:abandoned · 未打穿(已获大奖励不追回——同失败型结算)", String(s10.get("result", "")) == "abandoned" and not bool(s10.get("cleared", true)))
# ---- 专长冒烟(feats 四级 · 案D 池接线 · V3 订阅面逐件三列)----
# ---- 专长冒烟(feats 四级 · 案D 池接线 · V3 订阅面逐件三列)----
func _smoke_feats() -> void:
print("--- 专长重设计实装(feat_table 四级 · 专长带池接线 · 订阅面逐件)---")
# ① 现行表牌库构成:30 件全量入池——102 数值令牌+30 relic 令牌(四级 rarity 直映射)
Kit.check("feat_table 已加载(30 件=基础 27+迁入 2+卡② 完美保险 1——迭代 31 卡② 换代)", _db.feats != null and (_db.feats.rows as Array).size() == 30)
Kit.check("feat_table.validate 零违例(四级 rarity/cap 域/trigger 词表/行键白名单全过)", _db.feats.validate().is_empty())
var a_r0 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(71))
Kit.check("专长带场景:冒险启动(seed 71)", a_r0.start_adventure(71, {"first_script": true}))
var feat_tokens := 0
var tier_counts := {}
for t in a_r0._curio_deck:
if String((t as Dictionary).get("kind", "")) == "relic":
feat_tokens += 1
var tk := String((t as Dictionary).get("tier", ""))
tier_counts[tk] = int(tier_counts.get(tk, 0)) + 1
Kit.check("专长带入池:30 件四级直映射(普通 12/优秀 9/稀有 9/传说 0)——牌库 102+30=132〔专长批 V2+卡② 完美保险迁入〕", feat_tokens == 30 and int(tier_counts.get("common", 0)) == 12 and int(tier_counts.get("excellent", 0)) == 9 and int(tier_counts.get("rare", 0)) == 9 and a_r0._curio_deck.size() == 132)
# ② 旧 27 件零残留钉面(V2——arcane_ember 系旧件表首行;全仓 grep 零残留归静态核查)
Kit.check("旧 27 件已退役:arcane_ember/magma_body 表内零残留〔专长批 V2〕", _db.feats.row_by_id("arcane_ember").is_empty() and _db.feats.row_by_id("magma_body").is_empty())
Kit.check("迁出收尾:treasure_items 无 venom_edge/seal_burst 行(同调目录自然收窄)〔专长批 V13〕", _db.treasure_items.row_by_id("venom_edge").is_empty() and _db.treasure_items.row_by_id("seal_burst").is_empty())
# ③ 契约面静态钉扎(V18/V19/V21/V22——人工核对 V20/V21 裁决面以外的可机检半)
var kinds_ok := true
for t in a_r0._curio_deck:
var kk := String((t as Dictionary).get("kind", ""))
if kk != "relic" and not _db.adventure.CURIO_KINDS.has(kk):
kinds_ok = false
Kit.check("层级纪律:牌库 kind 全集=CURIO_KINDS+relic(秘宝 item_id 零入池)〔专长批 V18〕", kinds_ok)
var rewrite_rows := 0
for row_v in (_db.feats.rows as Array):
for tv in ((row_v as Dictionary).get("triggers", []) as Array):
if String(((tv as Dictionary).get("do", {}) as Dictionary).get("kind", "")) == "rewrite_board":
rewrite_rows += 1
Kit.check("棋盘事实不可变:新 30 行零棋盘改写类订阅(rewrite_board 已除名消费面)〔专长批 V19〕", rewrite_rows == 0)
var bad_words := 0
for row_v in (_db.feats.rows as Array):
var r_d: Dictionary = row_v as Dictionary
for txt_v in [String(r_d.get("display_name", "")), String(r_d.get("desc", ""))]:
if String(txt_v).contains("雷") or String(txt_v).contains("旗"):
bad_words += 1
Kit.check("对外命名纪律:30 行 display_name/desc 零「雷/旗」字面(封印/陷阱风味系)〔专长批 V21 初筛〕", bad_words == 0)
Kit.check("数值纪律:陷阱爆发倍率>1 · 血蜜回复<典型陷阱伤 · 石肤上限 3 在册(结构锚形态内 provisional)〔专长批 V22 机检半〕",
float((_db.feats.row_by_id("trap_burst").get("triggers", [{}])[0] as Dictionary).get("do", {}).get("mult", 0)) > 1.0
and int((_db.feats.row_by_id("blood_honey").get("triggers", [{}])[0] as Dictionary).get("do", {}).get("value", 99)) < 5
and int((_db.feats.row_by_id("stone_skin").get("triggers", [{}])[0] as Dictionary).get("do", {}).get("cap", 0)) == 3)
# ④ 临时表注入(新 schema 形态——零 archetype/cap 直用):5 行 relic 入牌库/兑付/踢池/断点往返
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"}}]},
]
Kit.check("新 schema 构造表 validate 零违例(四级 rarity/cap 字段/词表全过——流派标签已退役零 archetype)", rt.validate().is_empty())
var live_rows: FeatTable = _db.feats
_db.feats = rt
var a_r = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(72))
Kit.check("专长带场景:冒险启动(seed 72——注入 5 行临时表)", a_r.start_adventure(72, {"first_script": true}))
var deck_relic := 0
for t in a_r._curio_deck:
if String((t as Dictionary).get("kind", "")) == "relic":
deck_relic += 1
Kit.check("专长带入池:5 行 relic 每件 1 张(四级 rarity 直映射)——牌库 102+5=107", deck_relic == 5 and a_r._curio_deck.size() == 107)
# 渠道白名单断言(渠道轴定位:通关渠道只出专长/节点渠道零专长)
var feat_seen: Dictionary = {}
var node_relic_seen := false
for i in 60:
for t in a_r._draw_curio_candidates("level_clear"):
if String((t as Dictionary).get("kind", "")) == "relic":
feat_seen[String(t.get("feat_id", ""))] = true
for t in a_r._draw_curio_candidates("reward_node"):
if String((t as Dictionary).get("kind", "")) == "relic":
node_relic_seen = true
Kit.check("通关渠道只出专长(60 次重抽 ≥2 种不同 relic——身份键 feat_id 同档可多件)", feat_seen.size() >= 2)
Kit.check("节点渠道零专长(60 次重抽零 relic——专长在非 Boss 渠道只以专长凭证形态出现)", not node_relic_seen)
# T5 专长凭证行为断言(A5 拿到即开 / A6 保底计数——契约④四口径;独立 runner 防污染 a_r 牌库)
var a_t = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(91))
a_t.start_adventure(91, {"first_script": true})
var stock_before := {}
for kk in ["attack", "gp", "heal"]:
stock_before[kk] = a_t.pool_stock(kk)
a_t._node_curio_pending = {"candidates": [{"kind": "feat_ticket", "tier": "excellent"}, {"kind": "attack", "tier": "common"}]}
Kit.check("A5 专长凭证兑付受理(节点渠道——拿到即开)", a_t.apply_node_curio("feat_ticket", "excellent"))
var tkt_cands: Array = (a_t.node_curio_pending_state().get("candidates", []) as Array)
var tkt_all_relic := tkt_cands.size() > 0
for t in tkt_cands:
if String((t as Dictionary).get("kind", "")) != "relic":
tkt_all_relic = false
Kit.check("A5 拿到即开——停点候选已替换为专长三选一(全 relic)", tkt_all_relic)
Kit.check("A5 零摘牌——专长凭证兑付不动令牌库(不进持有集)", a_t.pool_stock("attack") == int(stock_before["attack"]) and a_t.pool_stock("gp") == int(stock_before["gp"]))
Kit.check("A5 无专长凭证残留持有集", not (a_t.feats_held() as Array).has("feat_ticket"))
var feats_n := (a_t.feats_held() as Array).size()
a_t.apply_node_curio("relic", String((tkt_cands[0] as Dictionary).get("tier", "rare")), String((tkt_cands[0] as Dictionary).get("feat_id", "")))
Kit.check("A5 专长面兑付受理(专长凭证开出候选可正常入持有集)", (a_t.feats_held() as Array).size() == feats_n + 1)
# A6 保底:pity 直填 5 → 下次展示必出(第 6 次必出口径);限购满 → 恒不触发
a_t._feat_ticket_sold = 0 # A5 已兑付一枚专长凭证(sold=1=limit)——A6 段重置计数隔离验证
a_t._feat_ticket_pity = int(a_t.db.adventure.feat_ticket_pity_step)
var rg_t: RandomNumberGenerator = a_t.rng.open_stream("reward_pick")
Kit.check("A6 保底触发(pity=5 → 第 6 次展示必出——概率无关)", a_t._feat_ticket_consider(rg_t))
Kit.check("A6 触发后清零", a_t._feat_ticket_pity == 0)
a_t._feat_ticket_sold = int(a_t.db.adventure.feat_ticket_limit)
a_t._feat_ticket_pity = int(a_t.db.adventure.feat_ticket_pity_step)
Kit.check("A6 限购满不再触发(保底冻结)", not a_t._feat_ticket_consider(rg_t))
a_t._feat_ticket_sold = 0
a_t._feat_ticket_pity = 0
var offered := false
for i in 200:
if a_t._feat_ticket_consider(rg_t):
offered = true
break
Kit.check("A6 概率面存在(chance=20% 200 展示内必出现——零触发=概率面失守)", offered)
a_r._curio_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.pick_curio("relic", "rare", "x3") and (a_r.feats_held() as Array).has("x3") and a_r.pool_stock("relic") == deck_relic - 1)
# 断点往返三条(基点 1420–1426:relics 快照/计数器键/余库逐枚还原)按主会话裁决②改判集成宿主——
# 驱动代码(save_manager 装配+_on_level_finished+快照取读)随三条同去,集成批落位时重建。
_db.feats = live_rows # 还原现行表(共享 _db 防污染——后续冒烟段零感知)
# ⑤ V3 订阅面逐件(附表 A 三列——命中面=EffectEngine dispatch 直测;账本面=counter 直读;
# 落地行为=发射点施加面〔T1 施加段承载——本段按声明逐件钉命中与边界/反例〕)
var orch = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(73))
var fx = orch._fx
# counter 落账 helper(dispatch 只返回命中不落账——施加面同构:counter_add 逐条落账本 helper 承载)
var apply_fx := func(hits: Array) -> void:
for h in hits:
if String(h.get("kind", "")) == "counter_add":
var sv_c = h.get("set", null)
EffectEngine.counter_add(orch.counters_ctx, String(h.get("counter_id", "")), int(h.get("value", 0)) if sv_c == null else int(sv_c), sv_c != null)
# 开格系
fx.rebuild(["twin_pick"], [], [])
for i in 4:
var hits_dp: Array = fx.dispatch(orch.counters_ctx, "dig_paid", {"ap_cost": 1})
var refund := false
for h in hits_dp:
if String(h.get("kind", "")) == "grant_ap":
refund = true
EffectEngine.counter_add(orch.counters_ctx, "adv_twin_digs", 1)
if i == 1 or i == 3:
Kit.check("双管镐:第 %d 次付费挖格返 1 AP(每第 2 次——mod_eq 预评估口径)" % (i + 1), refund)
else:
Kit.check("双管镐反例:第 %d 次不返(未满 2 次)" % (i + 1), not refund)
fx.rebuild(["geologist"], [], [])
var geo_hits: Array = fx.dispatch(orch.counters_ctx, "dig_reveal", {"new_resource_type": true})
apply_fx.call(geo_hits)
Kit.check("地质学家:首开新类型集 1 点〔专长批 V3〕", orch._hits_of(geo_hits, "counter_add").size() == 1)
EffectEngine.counter_add(orch.counters_ctx, "geo_types", 2) # 手动基数补齐(row1 已落账 1+2=3——满 4 种的预评估现值)
var geo_hits2: Array = fx.dispatch(orch.counters_ctx, "dig_reveal", {"new_resource_type": true})
apply_fx.call(geo_hits2)
Kit.check("地质学家:满 4 种(预评估现值 3+本格)→揭示 1 格·计数清零〔专长批 V3〕", orch._hits_of(geo_hits2, "reveal_tiles").size() == 1 and EffectEngine.counter(orch.counters_ctx, "geo_types") == 0)
var geo_hits3: Array = fx.dispatch(orch.counters_ctx, "dig_reveal", {"new_resource_type": false})
Kit.check("地质学家反例:同类型重复首开不叠点〔专长批 V3〕", orch._hits_of(geo_hits3, "counter_add").is_empty())
fx.rebuild(["hard_hat"], [], [])
apply_fx.call(fx.dispatch(orch.counters_ctx, "dig_reveal", {}))
apply_fx.call(fx.dispatch(orch.counters_ctx, "dig_reveal", {}))
Kit.check("安全帽:开安全格攒层 ×2〔专长批 V3〕", EffectEngine.counter(orch.counters_ctx, "helm_layers") == 2)
var hh_hits: Array = fx.dispatch(orch.counters_ctx, "mine_hit", {"damage": 6})
var hh_reduce := 0
for h in hh_hits:
if String(h.get("kind", "")) == "damage_reduce":
hh_reduce = EffectEngine.counter(orch.counters_ctx, String(h.get("value_counter", "")))
Kit.check("安全帽:踩雷耗层减伤=层值 2(一次清零随施加面 set 行)〔专长批 V3〕", hh_reduce == 2)
fx.rebuild(["skip_mining"], [], [])
var sk_zero: Array = fx.dispatch(orch.counters_ctx, "dig_reveal", {"is_blank_zero": true})
apply_fx.call(sk_zero)
Kit.check("跳格采掘:空白 0 格攒 1 点〔专长批 V3〕", orch._hits_of(sk_zero, "counter_add").size() == 1)
EffectEngine.counter_add(orch.counters_ctx, "skip_pts", 1) # 手动基数补齐(已落账 1+1=2——满 3 点的预评估现值)
var sk_full: Array = fx.dispatch(orch.counters_ctx, "dig_reveal", {"is_blank_zero": true})
apply_fx.call(sk_full)
Kit.check("跳格采掘:满 3 点→揭示 1 格〔专长批 V3〕", orch._hits_of(sk_full, "reveal_tiles").size() == 1)
Kit.check("跳格采掘反例:普通开格不计点〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.counters_ctx, "dig_reveal", {"is_blank_zero": false}), "counter_add").is_empty())
fx.rebuild(["blast_accounting"], [], [])
for i in 5:
var ba_hits: Array = fx.dispatch(orch.counters_ctx, "dig_paid", {"ap_cost": 1})
for h in ba_hits:
if String(h.get("kind", "")) == "counter_add":
EffectEngine.counter_add(orch.counters_ctx, String(h.get("counter_id", "")), 1)
Kit.check("爆破会计:本轮挖格花费≥5→+1 GP〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.counters_ctx, "round_end", {}), "grant_gp").size() == 1)
fx.rebuild(["frontier_miner"], [], [])
Kit.check("边陲矿工:边缘格产出 +1·中心格反例零产出〔专长批 V3〕",
orch._hits_of(fx.dispatch(orch.counters_ctx, "dig_reveal", {"is_edge_cell": true}), "grant_material").size() == 1
and orch._hits_of(fx.dispatch(orch.counters_ctx, "dig_reveal", {"is_edge_cell": false}), "grant_material").is_empty())
# 封印系
fx.rebuild(["seal_echo"], [], [])
apply_fx.call(fx.dispatch(orch.counters_ctx, "seal_hit", {}))
apply_fx.call(fx.dispatch(orch.counters_ctx, "seal_hit", {}))
var se_hits: Array = fx.dispatch(orch.counters_ctx, "round_end", {})
Kit.check("封印回响:标中计数≥2→轮末返 1 AP(下轮 queue)〔专长批 V3〕", orch._hits_of(se_hits, "grant_ap").size() == 1 and String((orch._hits_of(se_hits, "grant_ap")[0] as Dictionary).get("mode", "")) == "queue")
fx.rebuild(["seal_smith"], [], [])
var sm_remove := 0
for i in 3:
var sm_dispatch: Array = fx.dispatch(orch.counters_ctx, "seal_hit", {})
apply_fx.call(sm_dispatch)
for h in sm_dispatch:
if String(h.get("kind", "")) == "remove_mines":
sm_remove += 1
Kit.check("封印匠:标中每第 3 次→中立移除恰一次(频率闸死)〔专长批 V3〕", sm_remove == 1)
fx.rebuild(["seal_stake"], [], [])
Kit.check("封印桩:标中本轮 AC +N(round 域——round_start 清零)〔专长批 V3〕",
orch._hits_of(fx.dispatch(orch.counters_ctx, "seal_hit", {}), "ac_bonus").size() == 1
and String((orch._hits_of(fx.dispatch(orch.counters_ctx, "seal_hit", {}), "ac_bonus")[0] as Dictionary).get("mode", "")) == "round")
fx.rebuild(["warning_beacon"], [], [])
Kit.check("示警烟火:标中揭示 3×3 资源类型(不揭安危位置)〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.counters_ctx, "seal_hit", {}), "preview_area").size() == 1)
# 踩雷/受伤系
fx.rebuild(["pain_memory"], [], [])
apply_fx.call(fx.dispatch(orch.counters_ctx, "mine_hit", {}))
apply_fx.call(fx.dispatch(orch.counters_ctx, "mine_hit", {}))
var pm_hits: Array = fx.dispatch(orch.counters_ctx, "attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0})
var pm_bonus := 0
for h in orch._hits_of(pm_hits, "attack_bonus"):
pm_bonus = EffectEngine.counter(orch.counters_ctx, String(h.get("value_counter", "")))
Kit.check("疼痛记忆:攻击附加层值伤害=层值 2·用后清零(set 行随施加面)〔专长批 V3〕", pm_bonus == 2)
fx.rebuild(["fuse_wire"], [], [])
var fw_hits: Array = fx.dispatch(orch.counters_ctx, "damage_taken", {"kind": "terrain", "amount": 6})
var fw_shield := 0
for h in fw_hits:
if String(h.get("kind", "")) == "shield":
fw_shield += int(h.get("value", 0))
Kit.check("保险丝反例:单次 <5 不攒点不触发〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.counters_ctx, "damage_taken", {"kind": "terrain", "amount": 3}), "shield").is_empty())
for h in fx.dispatch(orch.counters_ctx, "damage_taken", {"kind": "terrain", "amount": 6}):
if String(h.get("kind", "")) == "counter_add":
EffectEngine.counter_add(orch.counters_ctx, String(h.get("counter_id", "")), int(h.get("value", 0)))
Kit.check("保险丝:首点不出盾(2 点才出——预评估口径)〔专长批 V3〕", fw_shield == 0)
fx.rebuild(["blood_insight"], [], [])
Kit.check("见血开窍:踩雷揭示 1 格〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.counters_ctx, "mine_hit", {}), "reveal_tiles").size() == 1)
fx.rebuild(["trap_instinct"], [], [])
apply_fx.call(fx.dispatch(orch.counters_ctx, "mine_hit", {}))
var ti_hits: Array = fx.dispatch(orch.counters_ctx, "attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0})
var ti_val := 0
for h in orch._hits_of(ti_hits, "attack_bonus"):
ti_val = EffectEngine.counter(orch.counters_ctx, String(h.get("value_counter", "")))
Kit.check("陷阱本能:踩雷本轮攻击 +2(attack_bonus 通道——面板加算)〔专长批 V3〕", ti_val == 2)
fx.rebuild(["chain_traps", "pain_memory"], [], [])
apply_fx.call(fx.dispatch(orch.counters_ctx, "mine_hit", {}))
var ct_before: int = EffectEngine.counter(orch.counters_ctx, "pain_layers")
apply_fx.call(fx.dispatch(orch.counters_ctx, "mine_hit", {}))
Kit.check("连环陷阱:同轮第 2 颗起每颗额外 +1 层(疼痛记忆同账)〔专长批 V3〕", EffectEngine.counter(orch.counters_ctx, "pain_layers") == ct_before + 2)
fx.rebuild(["trap_burst"], [], [])
var tb_hits: Array = fx.dispatch(orch.counters_ctx, "mine_hit", {"damage": 8})
Kit.check("陷阱爆发:全体敌雷伤×倍率直伤(blast_scaled——施加面值=damage×mult)〔专长批 V3〕",
orch._hits_of(tb_hits, "blast_scaled").size() == 1
and float((orch._hits_of(tb_hits, "blast_scaled")[0] as Dictionary).get("mult", 0)) > 1.0)
fx.rebuild(["blood_honey"], [], [])
Kit.check("血蜜:踩雷回复 N 生命〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.counters_ctx, "mine_hit", {}), "heal").size() == 1)
fx.rebuild(["stone_skin"], [], [])
var ss_hits: Array = fx.dispatch(orch.counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 1})
Kit.check("石肤:受伤 AC+1(本场域 cap 3——达上限不再涨由累积器封顶)〔专长批 V3〕",
orch._hits_of(ss_hits, "ac_bonus").size() == 1
and int((orch._hits_of(ss_hits, "ac_bonus")[0] as Dictionary).get("cap", 0)) == 3)
fx.rebuild(["last_stand"], [], [])
Kit.check("背水:生命≤50% 减伤·过半血反例零减伤〔专长批 V3〕",
orch._hits_of(fx.dispatch(orch.counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 1, "hp_ratio": 40}), "damage_reduce").size() == 1
and orch._hits_of(fx.dispatch(orch.counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 2, "hp_ratio": 80}), "damage_reduce").is_empty())
# 跨族桥
fx.rebuild(["breaker_strike"], [], [])
Kit.check("破封一击:挖开带封印危险格 +1 层·无封印反例零触发〔专长批 V3〕",
orch._hits_of(fx.dispatch(orch.counters_ctx, "mine_hit", {"dug_under_seal": true}), "counter_add").size() == 1
and orch._hits_of(fx.dispatch(orch.counters_ctx, "mine_hit", {"dug_under_seal": false}), "counter_add").is_empty())
# 节奏系
fx.rebuild(["map_exchange"], [], [])
var me_hits: Array = fx.dispatch(orch.counters_ctx, "dig_stop", {"ap_left": 4})
apply_fx.call(me_hits)
EffectEngine.counter_add(orch.counters_ctx, "stop_swap", 4) # value_event="ap_left" 解析归施加面(action_economy 载荷取值)——冒烟手动落账
Kit.check("弃图兑换:停手剩余 AP≥3 入账〔专长批 V3〕", orch._hits_of(me_hits, "counter_add").size() == 1)
var me_atk := 0
for h in orch._hits_of(fx.dispatch(orch.counters_ctx, "attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0}), "attack_bonus"):
me_atk = EffectEngine.counter(orch.counters_ctx, String(h.get("value_counter", "")))
Kit.check("弃图兑换:折为攻击伤害=剩余 AP 全额·用后清零〔专长批 V3〕", me_atk == 4)
Kit.check("弃图兑换反例:剩余 AP<3 不触发〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.counters_ctx, "dig_stop", {"ap_left": 2}), "counter_add").is_empty())
fx.rebuild(["last_drop"], [], [])
var ld_hits: Array = fx.dispatch(orch.counters_ctx, "round_start", {"round": 2, "ap_exhausted_last": true})
Kit.check("一滴不剩:上轮 AP 耗尽→+1 AP〔专长批 V3〕", orch._hits_of(ld_hits, "grant_ap").size() == 1)
Kit.check("一滴不剩反例:上轮有余不发〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.counters_ctx, "round_start", {"round": 3, "ap_exhausted_last": false}), "grant_ap").is_empty())
fx.rebuild(["market_close"], [], [])
for i in 8:
for h in fx.dispatch(orch.counters_ctx, "dig_reveal", {}):
if String(h.get("kind", "")) == "counter_add":
EffectEngine.counter_add(orch.counters_ctx, String(h.get("counter_id", "")), 1)
Kit.check("收盘:本轮开格≥8→+1 GP〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.counters_ctx, "round_end", {}), "grant_gp").size() == 1)
# 战斗系
fx.rebuild(["execution_scholar"], [], [])
Kit.check("斩首学者:击杀揭示 1 格〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.counters_ctx, "kill", {"target": "敌", "overflow": 0}), "reveal_tiles").size() == 1)
fx.rebuild(["executioner"], [], [])
var ex_hits: Array = fx.dispatch(orch.counters_ctx, "enemy_hp_threshold", {"unit": "e1", "pct": 30, "is_boss": false})
Kit.check("处刑台:敌血量≤30% 挂单位标记加伤·阈值外反例零命中〔专长批 V3〕",
orch._hits_of(ex_hits, "target_attack_bonus").size() == 1
and orch._hits_of(fx.dispatch(orch.counters_ctx, "enemy_hp_threshold", {"unit": "e1", "pct": 60, "is_boss": false}), "target_attack_bonus").is_empty())
fx.rebuild(["field_dressing"], [], [])
Kit.check("战地包扎:战斗胜利回复 N 生命(battle_won 新挂点)〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.counters_ctx, "battle_won", {"round": 2}), "heal").size() == 1)
# 契约尖点
fx.rebuild(["blood_tax"], [], [])
Kit.check("血税:双向同局账本(踩陷阱−2max_hp/封印命中+1atk——bonus_delta 战末汇入)〔专长批 V3〕",
orch._hits_of(fx.dispatch(orch.counters_ctx, "mine_hit", {}), "bonus_delta").size() == 1
and String((orch._hits_of(fx.dispatch(orch.counters_ctx, "seal_hit", {}), "bonus_delta")[0] as Dictionary).get("axis", "")) == "atk")
# 迁入 2 件
fx.rebuild(["venom_edge"], [], [])
Kit.check("淬毒刃纹(迁入):克制正面(magic_vs_heavy 矩阵值>0)追加·中性面反例零触发〔专长批 V3〕",
orch._hits_of(fx.dispatch(orch.counters_ctx, "clamp_check", {"matchup": "magic_vs_heavy"}), "modify_ticket").size() == 1
and orch._hits_of(fx.dispatch(orch.counters_ctx, "clamp_check", {"matchup": "normal_vs_light"}), "modify_ticket").is_empty())
fx.rebuild(["seal_burst"], [], [])
Kit.check("封印爆裂(迁入):标中全体敌群伤〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.counters_ctx, "seal_hit", {}), "open_ticket").size() == 1)
# 零持有
var orch0 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(74))
orch0._fx.rebuild([], [], [])
Kit.check("零持有订阅=零命中(relic 关着零扰动——伤害管线既有行为逐位不变)",
orch0._fx_emit("attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0}).is_empty()
and orch0._fx_emit("dig_reveal", {"cell": Vector2i(1, 1), "round": 1, "is_first_dig_of_battle": true, "terrain_kind": []}).is_empty()
and orch0._fx_emit("mine_hit", {"damage": 5}).is_empty()
and orch0._fx_emit("battle_won", {"round": 1}).is_empty())
# ⑦c 特性持有呈现段(C 区「特性」节;呈现面在 06 在册)
var panel = load("res://presentation/views/hero_info_panel_view.gd").new()
panel.set_feat_holdings([])
Kit.check("特性段空态隐藏(视图空态纪律同特性段)", not panel._feat_section.visible)
panel.set_feat_holdings(["疼痛记忆 ×1", "双管镐 ×1"])
Kit.check("特性段持有呈现(逐件行常驻可读——「局内奖励选了什么看不到」缺口承载)", panel._feat_section.visible and panel._feat_list.get_child_count() == 2)
Kit.check("特性卡文案表值化:desc 行字段直读(零硬编码映射)", String(load("res://presentation/battle_narrator.gd").feat_card_text({"feat_id": "pain_memory", "display_name": "疼痛记忆", "desc": "触陷阱攒 1 层"})).contains("触陷阱攒 1 层"))
# 手工场景驱动:L1 全程 + L2 推进到商店房(_drive_level 复用 + 兑付确认/选路/大奖励确认手工编排)
# 手工场景驱动:L1 全程 + L2 推进到商店房(_drive_level 复用 + 兑付确认/选路/大奖励确认手工编排)
func _adv_drive_to_shop(a, st: Dictionary, autoplay) -> bool:
var guard := 24
while guard > 0 and String(st["result"]) == "":
guard -= 1
autoplay._drive_level(a.runner(), st, null, "normal", "min")
if String(st["result"]) != "":
return false
if a._pending_next_level:
# 三选一停点确认(确定性选件后推进,与 run_adventure 自动策略同口径;TP0 令牌化)
if not a._curio_pending.is_empty():
var bcands: Array = a._curio_pending["candidates"]
var bpick: Dictionary = autoplay._pick_token(bcands)
a.pick_curio(String(bpick.get("kind", "")), String(bpick.get("tier", "")), String(bpick.get("feat_id", "")))
a.continue_to_next_level()
continue
var r = a.runner()
if String(r.current_node().get("type", "")) == "shop":
return true
if r.awaiting_choice():
var ids: Array = r.next_candidate_ids()
ids.sort()
r.pick_next_node(int(ids[0]))
continue
var t := String(r.current_node().get("type", ""))
if t == "reward":
# 奖励节点三选一(确定性选件、替换放弃新件、兑付确认回图;TP0 令牌化)
var ndraw: Dictionary = a.node_curio_candidates()
var ncands: Array = ndraw.get("candidates", []) as Array
if not ncands.is_empty():
a.present_node_curio(ncands)
var npick: Dictionary = autoplay._pick_token(ncands)
a.apply_node_curio(String(npick.get("kind", "")), String(npick.get("tier", "")), String(npick.get("feat_id", "")))
r.continue_after_node()
elif t == "rest":
r.continue_after_node()
return false
# 手工场景状态袋(_drive_level/_autoplay_battle 消费的最小字段族——run_adventure st 的同构子集)
# 手工场景状态袋(_drive_level/_autoplay_battle 消费的最小字段族——run_adventure st 的同构子集)
func _adv_st() -> Dictionary:
return {"result": "", "hp_left": 0, "battles": 0, "rounds": 0, "hero_dmg": 0,
"rest_healed": 0, "hp_before_rest": -1, "defeat_node_index": -1, "defeat_node_type": "",
"boss_depth": -1, "hp_at_boss": -1, "rows": [], "timeout": false, "log_path": "",
"policy": "normal", "refreshes": 0, "mine_hits": 0, "visited_ids": [], "choices": 0}
# ---- 未开格类型微光·数据侧断言(V1 作用集恒等/V2 九型归组封闭/V7 零玩法写入)----
# 夹具纪律:身份回调形状与 reward_economy.query_identity 同构({reward_id, cell_type, tier});
# 位点覆盖表 _glow_type_overrides(Vector2i → cell_type)+封锁集 _glow_blocked(Vector2i → true,
# 返回 null = 不在摇号清单的封锁格)。哈希兜底按坐标确定性散列——全缸可复现。
# 手搓战斗状态袋单点(7 处字面量收在一处;_autoplay_battle 直接消费的最小字段族。
# 大字段族同构袋仍走 _adv_st()——_drive_level/_autoplay_battle 全字段消费面)
func _battle_st(policy: String) -> Dictionary:
return {"result": "", "rounds": 0, "refreshes": 0, "mine_hits": 0, "hero_dmg": 0, "policy": policy}
# ==================== 敌人技能池(开轮摇号出招系统) ====================
# 六项验收断言(proposal-skill-pool.md〔提案已吸收退役 git a6833b6a——现行事实源=敌人系统.md 技能池节〕 §8 批 1):同种子回放逐位复现 / 单敌轨迹独立复放 /
# EV 抽样对锚 / 死亡打断作废 / 空池不可能 / 加载校验拒绝。
# 冒烟隔离用全新 RngService(同 master 种子——回放确定性纪律)
func _fresh_rng(seed_val: int):
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(seed_val)
return rng
# ==================== GP+商店房冒烟 ====================
# ① GP 到账(战斗完成入账恰一次 + Boss 关级加值)② escrow 隔离(格面兑付入缓冲/破财不透支)
# ③ 商店三收口(购买出账/加算区/余额不足拒/离开/收束清零)④ 图不变式(商店必经位/跨表行集迁移)
# ⑤ 九型与 gp_drop 流(奖励字典 18 格 + 黄金向量)。
# ---- ① GP 冒险钱包·到账 ----
# 模板感知取节点表(row.templates 非空取模板 0=现行固定图,保持既有断言语义)
func _level_active_nodes(level_index: int) -> Array:
for row in _db.level.rows:
if int(row.level_index) == level_index:
var tpls: Array = row.get("templates", [])
if not tpls.is_empty():
return tpls[0].get("nodes", [])
return row.get("nodes", [])
return []
@@ -232,8 +232,10 @@ func _smoke_power_interval() -> void:
for i in range(6):
eligible.append({"cell": Vector2i(i, 0)})
var seen := {}
var draws := {} # rid → 总抽数(流动判据用——seen 只记不同值)
for rid in interval_rows.keys():
seen[rid] = {} # rid → {roll 值集}(区间行随表注册)
draws[rid] = 0
var in_domain := true
var fixed_ok := true
for trial in range(24):
@@ -246,6 +248,7 @@ func _smoke_power_interval() -> void:
if interval_rows.has(rid):
var v := int(snap.power)
seen[rid][v] = true
draws[rid] = int(draws.get(rid, 0)) + 1
if v < interval_rows[rid][0] or v > interval_rows[rid][1]:
in_domain = false
elif int(snap.power) != _row_power(rid):
@@ -255,7 +258,10 @@ func _smoke_power_interval() -> void:
var c_rid := String(cs.reward_id)
if not seen.has(c_rid):
seen[c_rid] = {}
if not draws.has(c_rid):
draws[c_rid] = 0
seen[c_rid][cv] = true
draws[c_rid] = int(draws.get(c_rid, 0)) + 1
if interval_rows.has(c_rid) and (cv < interval_rows[c_rid][0] or cv > interval_rows[c_rid][1]):
in_domain = false
Kit.check("24 板全区间行快照 power ∈ [power_min, power_max](含清图槽)", in_domain)
@@ -271,7 +277,7 @@ func _smoke_power_interval() -> void:
if lo == hi:
if samples > 0 and not (samples == 1 and seen[rid].has(lo)):
fixed_rows_ok = false
elif samples >= 2 and seen[rid].size() < 2:
elif int(draws.get(rid, 0)) >= 2 and seen[rid].size() < 2:
variance_ok = false
Kit.check("区间行上下阶实际流动(≥2 样本的区间行须 ≥2 个不同 roll 值)", variance_ok)
Kit.check("定值行(lo=hi)roll 恒等表值", fixed_rows_ok)
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,734 @@
# tests/unit/core/meta_progression_forge_test.gd — 元进度与锻造单元测试宿主
# 断言面 = 六套(批 B 断言迁移自 smoke_all.gd,G9 施工组;六套按源编排序迁入):
# 元进度·树购买面(购买受理/持久/余额不足拒绝/未知节点拒绝——行价=表值 flat 单价)
# 元进度·收束入账与工坊(Boss 两档/钥石不丢/秘宝解锁/摘要副本与 cap/同调位/开局传参面)
# 元进度·注入可见与白板(hero_override 公式面/三轴白板逐位一致)
# 锻造工坊·两根类型轴所有权移交(静态表两轴删除/写口族/并联双通道/两币分工/注入接线/三层 AC)
# 天赋树(12 节点 schema 反例/购买面行门槛/Fork 一节点双态/彩蛋觉醒)
# 天赋树六键行为(先手窗/危险直觉限次/守侧五场景/财富之道聚散)
# 协议照单元侧惯例:extends SceneTree+Kit.check 主通道;--script 模式无 autoload,
# ConfigDB 显式实例化构造注入;表加载失败不中止(沿被迁断言面的源语义——诊断在此展开,
# 表值变异的红由具体断言落,承 G2/G3/G4/G5 同款);全局 class_name(Attunement/EffectEngine/
# EffectContext)裸用(--script 模式全局类在册,G3 先例)。
# 运行:godot --headless --path game --script res://tests/unit/core/meta_progression_forge_test.gd
extends SceneTree
const Kit := preload("res://tests/unit/test_kit.gd")
const ConfigDBScript := preload("res://core/config_db.gd")
const AutoPlayer := preload("res://smoke/auto_player.gd") # 自动玩家策略单一实现(迁移期按现路径 preload——账本 §五夹具前提)
var _db
func _init() -> void:
_db = ConfigDBScript.new()
# 表加载失败不中止(承 G2/G3/G4/G5 同款:表损坏的红落在具体断言上——
# 诊断在此展开,后续断言按坏表实况逐条验红;collect_only=true 下成员照常赋值可评)
var load_ok: bool = _db.load_tables("res://data/tables", true)
if not load_ok:
push_error("[meta_progression_forge_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_meta 薄壳序 + _run_core_smoke 尾段序)
_meta_pricing_and_buy()
_meta_settle_and_workshop()
_meta_injection()
_smoke_forge_axes()
_smoke_tree()
_smoke_tree_rules()
quit(Kit.finish("meta_progression_forge_test"))
# ---- 隔离夹具(smoke_all.gd helper 的宿主自持私有副本——源文件收尾整体删除,宿主自给自足)----
# 冒烟隔离存档名册(段首清陈旧 + 段尾清理——契约:测试不污染 user://save.json 真实档)
const _META_SMOKE_PATHS := ["user://smoke_meta_buy.json", "user://smoke_meta_dry.json",
"user://smoke_meta_settle.json", "user://smoke_meta_workshop.json",
"user://smoke_meta_cap.json", "user://smoke_meta_white.json",
"user://smoke_meta_attune.json", "user://smoke_meta_attune2.json",
"user://smoke_meta_tree.json", "user://smoke_meta_career.json",
"user://smoke_meta_career_v5.json", "user://smoke_meta_treasure_unlock.json", "user://smoke_meta_treasure_blank.json",
"user://smoke_meta_forge.json", "user://smoke_meta_forge2.json", "user://smoke_meta_forge3.json",
"user://smoke_meta_forge4.json", "user://smoke_meta_forge5.json"]
func _meta_smoke_cleanup() -> void:
for p in _META_SMOKE_PATHS:
for suffix in ["", ".tmp", ".corrupt.bak"]:
if FileAccess.file_exists(p + suffix):
DirAccess.remove_absolute(p + suffix)
func _meta_new_save(path: String):
var sv = load("res://core/save_manager.gd").new(path)
sv.load_from_disk()
return sv
# 冒烟隔离用全新 RngService(同 master 种子——回放确定性纪律)
func _fresh_rng(seed_val: int):
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(seed_val)
return rng
# ---- 全战斗回放驱动(过渡副本——G6 属主件 `_run_battle`/`_auto_play_round` 的宿主侧一致副本:
# forge_axes「enc 两轴直达 spawn 英雄快照」需真实流驱动而 G6 宿主未建;smoke_all.gd 原件保留
# 供未迁套消费,收尾批与原件去重。副本去 G6 专属「收束相位回 Idle」断言——该条随 `_run_battle`
# 本体迁其宿主,本副本保持零断言纯驱动形态)----
func _run_battle(L: int, D: int, axes: Dictionary = {}) -> Dictionary:
# 每遍全新 RngService(同 master 种子)——回放断言的隔离前提(地图战斗资源 AC"两套全新 RngService+地图战斗资源"同款)
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(42)
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
var out := {"stream": [], "events": [], "rounds": 0, "result": "", "terminated": false}
settlement.step_row.connect(func(row): out.stream.append(row))
settlement.battle_finished.connect(func(payload): out.result = payload.get("result", ""))
settlement.minesweeper.cell_revealed.connect(func(evt): out.events.append(evt))
settlement.minesweeper.seal_judged.connect(func(evt): out.events.append(evt))
settlement.minesweeper.round_end_board.connect(func(p): out.events.append({"ev": "round_end_board", "path_tag": p.path_tag}))
settlement.action_economy.round_end_stopped.connect(func(p): out.events.append({"ev": "round_end_stopped", "path_tag": p.path_tag}))
var enc := {"level_index": L, "node_depth": D, "hero_hp": _db.hero.max_hp}
enc.merge(axes, true) # 两根类型轴注入尾参(缺省零注入=回退面)
if not settlement.begin_battle(enc):
out.stream.append({"type": "verdict", "value": "begin_failed"})
return out
var max_rounds := 40
while out.result == "" and out.rounds < max_rounds:
out.rounds += 1
_auto_play_round(settlement, out)
if out.result == "":
out.result = "timeout"
out.terminated = out.result == "victory" or out.result == "defeat"
return out
func _auto_play_round(settlement, out: Dictionary) -> void:
var minesweeper = settlement.minesweeper
var action_economy = settlement.action_economy
var start_round: int = settlement.round_index
# 确定性自动玩家:挖最近的已探明邻接未开格;无可挖则停手(结算窗口由信号回调提交)
var guard := 400
while action_economy.phase == "Active" and settlement.round_index == start_round and guard > 0 and out.result == "":
guard -= 1
var plan := AutoPlayer.choose_dig(minesweeper)
if plan.is_empty():
break
if plan.hero_cell != minesweeper.hero_pos():
minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}])
if action_economy.phase != "Active" or settlement.round_index != start_round:
break
minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
if action_economy.phase == "Active" and settlement.round_index == start_round and out.result == "":
minesweeper.settle_batch([{"action": "stop"}])
# ---- 元进度 ②树购买面(购买受理/持久/余额不足拒绝/未知节点拒绝;
# 行价=表值 flat 单价)----
func _meta_pricing_and_buy() -> void:
print("--- 元进度·树购买面(隔离档 smoke_meta_buy/dry)---")
_meta_smoke_cleanup()
var sv = _meta_new_save(_META_SMOKE_PATHS[0])
var mp = load("res://core/meta_progression.gd").new(_db, sv)
# ③ 购买成功:级+1、余额−价;buy 内含写盘——reload 后持久(树全钥石单币)
sv.add_keystone(1000)
var bal0: int = sv.keystone_balance()
var price0: int = int(_db.tree_node("sp_hp").get("price", 0))
Kit.check("树购买受理(Lv 0→1、余额 %d−%d)" % [bal0, price0],
mp.tree_buy("sp_hp") and sv.tree_level("sp_hp") == 1 and sv.keystone_balance() == bal0 - price0)
var sv_r = _meta_new_save(_META_SMOKE_PATHS[0])
Kit.check("购买持久(reload 后级与余额不变)",
sv_r.tree_level("sp_hp") == 1 and sv_r.keystone_balance() == bal0 - price0)
# ③ 余额不足 / 未知节点:false 零副作用
var sv_dry = _meta_new_save(_META_SMOKE_PATHS[1])
var mp_dry = load("res://core/meta_progression.gd").new(_db, sv_dry)
var tn_before: Dictionary = sv_dry.tree_nodes()
var fs_before: Dictionary = sv_dry.fork_sides()
Kit.check("余额不足 false 零副作用(0 余额购买拒绝——三域零变化〔天赋树批 V3〕)",
not mp_dry.tree_buy("sp_hp") and sv_dry.keystone_balance() == 0
and sv_dry.tree_nodes() == tn_before and sv_dry.fork_sides() == fs_before)
Kit.check("未知节点 false 零副作用(词表外出域)", not mp_dry.tree_buy("no_such_node"))
# ---- 元进度 ⑥Boss 两档 ⑦钥石不丢 ⑧工坊解锁 ⑩摘要副本与 cap(合成摘要驱动——settle 纯账面零跑局)----
func _meta_settle_and_workshop() -> void:
print("--- 元进度·收束入账与工坊(隔离档 smoke_meta_settle/workshop/cap)---")
var sv = _meta_new_save(_META_SMOKE_PATHS[2])
var mp = load("res://core/meta_progression.gd").new(_db, sv)
var row1: Dictionary = _db.meta_boss_keystone_row(1)
var row2: Dictionary = _db.meta_boss_keystone_row(2)
# ⑥ 逐杀掉落收束入账(掉落已在冒险内逐杀入账〔summary.keystone_total 含全部〕,
# keystone_drops 明细随摘要归档;首杀标记仍由 apply 逐通关关次置位)
var d1: Dictionary = mp.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 1,
"stopped_at": 2, "keystone_total": int(row1.get("first_kill", 0)),
"keystone_drops": [{"level": 1, "first": true, "amount": int(row1.get("first_kill", 0))}],
"master_seed": 11})
var drop1: Dictionary = d1["boss_drops"][0]
Kit.check("新档首通 L1:boss_drops[0] {first:true, amount=first_kill}(验收5——明细随摘要归档)",
bool(drop1.get("first", false)) and int(drop1.get("amount", 0)) == int(row1.get("first_kill", -1))
and int(drop1.get("level", 0)) == 1)
Kit.check("明细 total = keystone_total(逐杀掉落全源——无二次派生加账)",
int(d1.get("total", 0)) == int(row1.get("first_kill", 0)))
Kit.check("余额 = 累计入账(逐杀掉落合计——无重复加账)",
sv.keystone_balance() == int(row1.get("first_kill", 0)))
# ⑥ 再通转复杀档(首杀标记置位后小额恒定不递减——标记面在 apply,金额面在逐杀裁决)
var d2: Dictionary = mp.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 1,
"stopped_at": 2, "keystone_total": int(row1.get("repeat_kill", 0)),
"keystone_drops": [{"level": 1, "first": false, "amount": int(row1.get("repeat_kill", 0))}],
"master_seed": 12})
var drop2: Dictionary = d2["boss_drops"][0]
Kit.check("再通 L1:{first:false, amount=repeat_kill}(首杀标记置位后转小额)",
not bool(drop2.get("first", true)) and int(drop2.get("amount", 0)) == int(row1.get("repeat_kill", -1)))
var d3: Dictionary = mp.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 1,
"stopped_at": 2, "keystone_total": int(row1.get("repeat_kill", 0)),
"keystone_drops": [{"level": 1, "first": false, "amount": int(row1.get("repeat_kill", 0))}],
"master_seed": 13})
Kit.check("重复杀第三轮恒定不递减(验收5——无衰减曲线)",
not bool(d3["boss_drops"][0].get("first", true)) and int(d3["boss_drops"][0].get("amount", 0)) == int(row1.get("repeat_kill", -1)))
# 混合归因:通关两关 → L1 重复 + L2 首杀(明细随摘要逐笔归档)
var d4: Dictionary = mp.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 2,
"stopped_at": 0, "keystone_total": int(row1.get("repeat_kill", 0)) + int(row2.get("first_kill", 0)),
"keystone_drops": [{"level": 1, "first": false, "amount": int(row1.get("repeat_kill", 0))},
{"level": 2, "first": true, "amount": int(row2.get("first_kill", 0))}],
"master_seed": 14})
Kit.check("两关通关逐关归因:L1 重复档 + L2 首杀档(first_kill 金额)",
not bool(d4["boss_drops"][0].get("first", true)) and bool(d4["boss_drops"][1].get("first", false))
and int(d4["boss_drops"][1].get("amount", 0)) == int(row2.get("first_kill", -1)))
# ⑦ 败北零通关摘要:余额只增不减(零入账)、clear_counts 不变
var bal_before: int = sv.keystone_balance()
var cc1_before: int = sv.clear_count(1)
var dz: Dictionary = mp.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 0,
"stopped_at": 1, "keystone_total": 0, "master_seed": 15})
Kit.check("败北零通关摘要:零入账零标记(钥石只增不减——验收1;败北局 clear_counts 不变)",
dz["boss_drops"].is_empty() and sv.keystone_balance() == bal_before and sv.clear_count(1) == cc1_before)
# ⑧ 秘宝解锁服务(V3 钥石购买/V4 条件解锁;注入=直调解锁写口+直置存档生涯计数,
# 不走正常资源积累流程——卡片 7.1 测试方式)
var sv_u = _meta_new_save(_META_SMOKE_PATHS[11])
var mp_u = load("res://core/meta_progression.gd").new(_db, sv_u)
sv_u.add_keystone(10)
Kit.check("条件件误购拒绝(购买件专属——契约 8 分配面)〔秘宝批 V3 反例〕",
not mp_u.buy_treasure_unlock("sky_eye") and not sv_u.item_unlocked("sky_eye")
and not mp_u.buy_treasure_unlock("ranged_pick"))
Kit.check("余额不足购买拒绝且零副作用(余额 10 < 价 25)〔秘宝批 V3〕",
not mp_u.buy_treasure_unlock("twin_loader") and sv_u.keystone_balance() == 10)
Kit.check("钥石购买解锁:余额足→解锁态落档+余额扣减正确(价 10)〔秘宝批 V3〕",
mp_u.buy_treasure_unlock("morning_order") and sv_u.item_unlocked("morning_order")
and sv_u.keystone_balance() == 0)
Kit.check("已解锁重复购买拒绝〔秘宝批 V3〕",
not mp_u.buy_treasure_unlock("morning_order"))
Kit.check("条件未达不解锁(远程镐=通关 1 次)〔秘宝批 V4〕",
mp_u.refresh_treasure_unlocks().is_empty() and not sv_u.item_unlocked("ranged_pick"))
sv_u._data["meta"]["adventure_cleared"] = 1 # 直置存档生涯计数(调试注入口径)
Kit.check("远程镐=adventure_cleared≥1 达标恰解锁一次+重复刷新幂等(C6:通关=打通整局冒险)〔秘宝批 V4〕",
(mp_u.refresh_treasure_unlocks() as Array).has("ranged_pick")
and sv_u.item_unlocked("ranged_pick") and mp_u.refresh_treasure_unlocks().is_empty())
sv_u._data["meta"]["boss_kill_count"] = 3
Kit.check("刷新执照=boss_kill_count≥3(阈值读 unlock_requires 行值不硬编)〔秘宝批 V4〕",
(mp_u.refresh_treasure_unlocks() as Array).has("refresh_license"))
sv_u._data["meta"]["adventure_cleared"] = 3
Kit.check("天眼单条件不足不解锁(AND 语义——cleared 3 达 boss 4 未达)〔秘宝批 V4〕",
not sv_u.item_unlocked("sky_eye"))
sv_u._data["meta"]["boss_kill_count"] = 4
Kit.check("天眼双条件 AND(通关≥3 且 Boss≥4)达标解锁〔秘宝批 V4〕",
(mp_u.refresh_treasure_unlocks() as Array).has("sky_eye"))
sv_u._data["meta"]["vision_used"] = 10
Kit.check("信标网络=真视类工具累计使用≥10(行为计数新域——use_treasure 受理成功累计)〔秘宝批 V4〕",
(mp_u.refresh_treasure_unlocks() as Array).has("beacon_network"))
var r_n := 0
for st_row in mp_u.treasure_shop_status():
r_n += 1 if bool(st_row.get("unlocked", false)) else 0
Kit.check("解锁态持久投影一致(reload 面同源读数;5 件=购买 1+条件 4)〔秘宝批 V4〕", r_n == 5)
# ⑩ recent_summaries:元素含 boss_drops 副本、最新在前、cap 10
var rs: Array = sv.recent_summaries()
Kit.check("摘要最新在前 + 元素含掉落明细副本(复盘可归因——验收5)",
rs.size() >= 5 and String(rs[0].get("result", "")) != "" and rs[0].has("boss_drops")
and (rs[1] as Dictionary).has("keystone_drops")
and int(rs[0].get("master_seed", -1)) == 15)
var sv_c = _meta_new_save(_META_SMOKE_PATHS[4])
var mp_c = load("res://core/meta_progression.gd").new(_db, sv_c)
for i in 12:
mp_c.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 0,
"stopped_at": 1, "keystone_total": 0, "master_seed": i})
Kit.check("摘要 cap 10 且最新在前(12 连 settle 截断后 seed 11 居首)",
sv_c.recent_summaries().size() == 10 and int(sv_c.recent_summaries()[0].get("master_seed", -1)) == 11)
# ⑪ 同调位账本(装配持久/满位禁选/Y 升级语义;写口 = MetaProgression 同调段)
var sv_a = _meta_new_save(_META_SMOKE_PATHS[6])
var mp_a = load("res://core/meta_progression.gd").new(_db, sv_a)
Kit.check("白板档同调位求值 = 0(0 起步裁决)且 Y=0(未购无刷新能力——P1 甲初始 0 同构)",
Attunement.slots(_db, sv_a) == 0 and int(mp_a.attune_camp_status()["refresh"]["charges"]) == 0)
Kit.check("满位禁选(Slots 0 时装配拒绝;兼未解锁拒绝前置——直置解锁后下方装配)〔秘宝批 V5〕",
mp_a.toggle_attunement("morning_order") == false)
sv_a.add_keystone(200)
sv_a.unlock_item("morning_order")
sv_a.unlock_item("twin_loader")
Kit.check("Y 升级购买入账(Lv1 → 每冒险 +1 次刷新)",
mp_a.buy_attune_refresh() and sv_a.attune_refresh_level() == 1
and int(mp_a.attune_camp_status()["refresh"]["charges"]) == 1)
mp_a.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 1,
"stopped_at": 2, "keystone_total": 0, "master_seed": 21})
Kit.check("通关 1 关 → Slots 恒 0(旧「按击杀 Boss 次数自动涨」条件已删——槽位唯一来源=树点亮)且 Boss 击杀总数=1(计数域保留)〔天赋树批 V14〕",
Attunement.slots(_db, sv_a) == 0 and sv_a.boss_kill_count() == 1)
sv_a.set_tree_level("att_socket1", 1)
Kit.check("树上点亮同调位 1 → Slots=1(槽位收编树节点——Q2 裁甲)",
Attunement.slots(_db, sv_a) == 1)
Kit.check("稀有件入普通格拒绝(件档 rank 3 > 装入位序 1——契约 2 档位格校验)〔秘宝批 V5〕",
not mp_a.toggle_attunement("sky_eye"))
Kit.check("装配持久写入 + 满位禁选(Slots 1 装第二件拒绝)〔秘宝批 V5 换主语〕",
mp_a.toggle_attunement("morning_order") and mp_a.toggle_attunement("twin_loader") == false)
Kit.check("卸下恒可再装配(自卸自装——营地换装=冒险开始前合法面,装配不可变只锁运行中)〔秘宝批换主语〕",
mp_a.toggle_attunement("morning_order") and mp_a.toggle_attunement("morning_order"))
var sv_a2 = _meta_new_save(_META_SMOKE_PATHS[7])
sv_a2.set_attunement_equipped(["morning_order"])
var mp_a2 = load("res://core/meta_progression.gd").new(_db, sv_a2)
Kit.check("装配关系读回持久(reload 后投影一致——写新件 id)〔秘宝批 V15〕",
(mp_a2.attune_camp_status()["equipped"] as Array).has("morning_order"))
# ⑫ 充能重置 / 刷新双计数器
var a12 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(12))
Kit.check("充能重置场景:装配冒险启动(seed 12)〔秘宝批 V8 换主语〕",
a12.start_adventure(12, {"attunement": ["incense_stick", "sky_eye"]}))
Kit.check("充能账本按装备 active_charged 件重置(新目录表值:线香 battle 级 1 次/天眼 level 级 1 次——周期重置点归 V8 逐形态面)", # 充能账住 RunLedger
int(a12.run_ledger.attune_charges.get("incense_stick", -1)) == 1 and int(a12.run_ledger.attune_charges.get("sky_eye", -1)) == 1)
a12._attunement = []
a12._attune_refresh_x = 1
a12._attune_refresh_y = 2
a12._curio_pending = {"candidates": [{"kind": "gp", "tier": "common"}, {"kind": "attack", "tier": "common"}, {"kind": "max_hp", "tier": "common"}], "payload": {}}
Kit.check("刷新双计数器——X 优先消耗(x1y2 → x0y2)",
a12.refresh_curio() and a12._attune_refresh_x == 0 and a12._attune_refresh_y == 2)
Kit.check("刷新双计数器——X 耗尽转 Y 池、池竭拒绝(x0y2 → y0 → 第四次 false 零副作用)",
a12.refresh_curio() and a12.refresh_curio() and a12._attune_refresh_y == 0
and a12.refresh_curio() == false)
a12._reset_refresh_x()
Kit.check("刷新 X 每碰三选一重置(不结转;新目录无 bonus_refresh 行源——X 机制保留恒 0 空转,存量加仓走 _refresh_stock=刷新练习 V8)〔秘宝批换代〕",
a12._attune_refresh_x == 0 and a12.attune_refresh_left() == 0)
# ⑬ 主菜单开局传参面(开新/续玩两入口同调三键随开局下传防回归:真实入口曾
# 三键恒空(缺陷①),冒烟夹具直呼 runner 参数面拦不住——此处直呼 main.build_adventure_params
# 单测统一构造面(static 免实例化整菜单场景;真实两入口共用本构造),取值与营地投影同源)
var sv_e = _meta_new_save(_META_SMOKE_PATHS[8])
sv_e.set_attunement_equipped(["morning_order"])
sv_e.set_attune_refresh_level(2)
sv_e.set_fork_side("fg_posture", "a")
var mp_e = load("res://core/meta_progression.gd").new(_db, sv_e)
var rules_e: Array = mp_e.tree_rule_keys()
Kit.check("开局传参夹具前置:Fork 侧 a 激活 rule 键非空 + Y=Lv2 行表值 2(防断言空转)",
rules_e.has("posture_first_strike") and mp_e.attune_refresh_charges() == 2)
var p_new: Dictionary = load("res://presentation/main.gd").build_adventure_params(sv_e, mp_e,
{"first_script": true, "treasure_unlocked": {}, "shop_mode": "pool"})
Kit.check("主菜单开新冒险参数面:同调装配 = 存档装配集(营地装了什么开局就带什么)",
(p_new.get("attunement", []) as Array) == (sv_e.attunement_equipped() as Array))
Kit.check("主菜单开新冒险参数面:Y 刷新池 = 当前等级行表值(营地升几档开局就给几次)",
int(p_new.get("attune_refresh_y", -1)) == mp_e.attune_refresh_charges())
Kit.check("主菜单开新冒险参数面:树 rule 键集 = Fork 激活侧 effects(点哪侧带哪侧)",
(p_new.get("tree_rules", []) as Array) == rules_e)
Kit.check("主菜单开新冒险参数面:入口特有键(first_script/shop_mode)随 extra 原样并入",
bool(p_new.get("first_script", false)) and String(p_new.get("shop_mode", "")) == "pool")
var p_resume: Dictionary = load("res://presentation/main.gd").build_adventure_params(sv_e, mp_e,
{"treasure_unlocked": {}})
Kit.check("主菜单继续上局参数面:三键与开新冒险同源同值(续玩不吃白板开局)",
(p_resume.get("attunement", []) as Array) == (sv_e.attunement_equipped() as Array)
and int(p_resume.get("attune_refresh_y", -1)) == mp_e.attune_refresh_charges()
and (p_resume.get("tree_rules", []) as Array) == rules_e)
# 续玩路径 tree_rules 受理断言:直呼 runner 续玩参数面
# (v2 旧断点夹具同式最小断点——smoke ⑫ 受理路径同形)验 _tree_rules 与传入一致。
var adr_res = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(19))
var bp_min: Dictionary = {"level_index": 1, "node_id": -1, "hero_hp": 40, "gp": 5,
"bonus": {"atk": 0, "max_hp": 0, "armor_class": 0}, "shop_purchased": {},
"template_pick": -1, "master_seed": 777, "rng_counter": 3, "keystone": 0,
"visited_ids": []}
Kit.check("继续上局受理面:resume_adventure 受理三键参数面(尾裁微批b 受理闭环——受理不再吃白板)",
adr_res.resume_adventure(bp_min, p_resume))
Kit.check("继续上局受理面:tree_rules 随参数面进 runner._tree_rules(消费面 set_tree_rules 下传现成)",
(adr_res._tree_rules as Array) == rules_e)
_meta_smoke_cleanup()
# ---- 元进度 ④注入可见 + 白板成立(验收 4/2/3——程序面;MC 逐位回归归 T7 全量验证)----
func _meta_injection() -> void:
print("--- 元进度·注入可见与白板(隔离档 smoke_meta_white/dry)---")
var sv = _meta_new_save(_META_SMOKE_PATHS[1])
var mp = load("res://core/meta_progression.gd").new(_db, sv)
# 树生命淬炼注入(公式:基础值+树淬炼 flat+锻造路线加值位预留 0——2 级 = +10;
# 注入走调试口径直置等级〔两分法——购买链已在 _smoke_tree 覆盖,此处隔离行门槛干扰〕)
sv.set_tree_level("sp_hp", 2)
Kit.check("生命淬炼 2 级 → hero_override().max_hp == Σ 节点 effects×等级(验收4 换代〔天赋树批 V12〕)",
int(mp.hero_override().get("max_hp", -1)) == 2 * int(_db.tree_node("sp_hp").get("effects", {}).get("max_hp", 0)))
Kit.check("攻/AC 树零来源(契约 9——树表零 atk/armor_class 行,override 二轴恒 0+预留位 0)",
int(mp.hero_override().get("atk", -1)) == 0 and int(mp.hero_override().get("armor_class", -1)) == 0)
Kit.check("命中线在账未注入(契约 6——override 无 hit 字段〔天赋树批 V13〕)",
not mp.hero_override().has("hit"))
var adr = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(920251))
Kit.check("注入参数冒险启动(start_adventure params 通道)",
adr.start_adventure(920251, {"hero_override": mp.hero_override()}))
var r = adr.runner()
var ov: Dictionary = mp.hero_override()
Kit.check("首战 hero_atk = 表值 + 永久强化 flat(begin_battle 同表达式——下一局能用到强化)",
int(r.effective_atk()) == int(_db.hero.atk) + int(ov.get("atk", 0)))
Kit.check("首战 hero_max_hp 与开局生命账本 = 表值 + 注入档",
int(r.effective_max_hp()) == int(_db.hero.max_hp) + int(ov.get("max_hp", 0))
and int(adr.ledger["hero_hp"]) == int(_db.hero.max_hp) + int(ov.get("max_hp", 0)))
# 白板:同种子下 空参数 = 零强化参数 = 纯表值(逐位一致——注入通道默认值漏改在此转红)
var adr_w = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(920251))
Kit.check("白板冒险启动(无 params 实参——B1 空参数路径)", adr_w.start_adventure(920251))
var sv_white = _meta_new_save(_META_SMOKE_PATHS[5])
var adr_z = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(920251))
Kit.check("零强化参数冒险启动(新档 hero_override 全零)",
adr_z.start_adventure(920251, {"hero_override": load("res://core/meta_progression.gd").new(_db, sv_white).hero_override()}))
var rw = adr_w.runner()
var rz = adr_z.runner()
Kit.check("白板成立:空参数 = 零强化参数 = 纯表值(atk/max_hp/ac 三轴逐位一致——验收 2/3)",
int(rw.effective_atk()) == int(_db.hero.atk) and int(rw.effective_max_hp()) == int(_db.hero.max_hp)
and int(rz.effective_atk()) == int(rw.effective_atk()) and int(rz.effective_max_hp()) == int(rw.effective_max_hp())
and int(rz.effective_ac()) == int(rw.effective_ac()))
Kit.check("ac 轴:白板恒表值、未换强化时注入零位移",
int(rw.effective_ac()) == int(_db.hero.armor_class) and int(r.effective_ac()) == int(_db.hero.armor_class))
_meta_smoke_cleanup()
# ---- 锻造工坊·两根类型轴所有权移交(V5 前半:hero.tres 两轴删除+锻造配置默认值)----
# 后半(build_adventure_params 注入默认值/选中覆盖)随 T4 注入接线同批断言;写口/解锁面归 T3。
func _smoke_forge_axes() -> void:
print("--- 锻造工坊·类型轴所有权移交(T2)---")
# V5:hero.tres 静态表两轴已删(全表加载零违例——config_db_test 同款闸;此处钉字段不在位)
Kit.check("hero.tres 无 attack_type/armor_type 字段(契约 2——静态表两轴删除)〔锻造工坊批 V5〕",
not ("attack_type" in _db.hero) and not ("armor_type" in _db.hero))
# 锻造配置默认值 = normal/light(常驻持有——与柜台解锁状态解耦,未点亮也照常可读)
Kit.check("锻造配置默认值 attack_type=normal / armor_type=light(常驻持有)〔锻造工坊批 V5〕",
_db.forge_default_attack_type() == "normal" and _db.forge_default_armor_type() == "light")
# 单位快照回退面:spawn 三参形态(零注入)= 锻造配置默认值
var uc = load("res://core/unit_container.gd").new(_db)
Kit.check("spawn 受理(三参形态——两轴缺省回退面)", uc.spawn(1, 0, int(_db.hero.max_hp)))
Kit.check("主角单位快照两轴 = 锻造配置默认值 normal/light(零注入回退——行为连续)〔锻造工坊批 V5〕",
String(uc.hero_unit().get("attack_type", "")) == "normal"
and String(uc.hero_unit().get("armor_type", "")) == "light")
# ---- T3 写口族(V2 购买/几何价 · V3 并联双通道 · V4 两币分工 · V6 选中切换)----
# 注入口径(两分法):add_keystone 置余额/直写存档域构造状态 → 直调写口 → 断余额/等级/读回
var sv2 = _meta_new_save(_META_SMOKE_PATHS[13])
var mp2 = load("res://core/meta_progression.gd").new(_db, sv2)
# V2①:长剑首购=20(0 级位几何价)、等级 0→1、扣币即盘、reload 持久
sv2.add_keystone(100)
Kit.check("长剑锻造首购:扣 20 钥石、等级 0→1(Q1——第一级=获得装备=首个 +1)〔锻造工坊批 V2〕",
mp2.forge_buy_level("sword") and sv2.forge_route_level("sword") == 1
and sv2.keystone_balance() == 80)
var sv2r = _meta_new_save(_META_SMOKE_PATHS[13])
Kit.check("购买持久(reload 后等级与余额不变——即时写盘)〔锻造工坊批 V2〕",
sv2r.forge_route_level("sword") == 1 and sv2r.keystone_balance() == 80)
# V2②:二级=30(1 级位);六路线独立(买长剑不动轻甲)
Kit.check("长剑二级购:扣 30、等级→2;轻甲独立零扰动(六路线各练各的)〔锻造工坊批 V2〕",
mp2.forge_buy_level("sword") and sv2.forge_route_level("sword") == 2
and sv2.keystone_balance() == 50 and sv2.forge_route_level("light") == 0)
# V2③:余额不足拒且零副作用(等级/选中/解锁三面零变化)
var sv2d = _meta_new_save(_META_SMOKE_PATHS[14])
var mp2d = load("res://core/meta_progression.gd").new(_db, sv2d)
var fr_dry: Dictionary = sv2d.forge_routes_snapshot()
Kit.check("余额不足购买拒绝且零副作用(0 余额——三域零变化)〔锻造工坊批 V2〕",
not mp2d.forge_buy_level("sword") and sv2d.keystone_balance() == 0
and sv2d.forge_routes_snapshot() == fr_dry)
# V2④:已满级拒购(cap 闸);未解锁拒购
sv2.set_forge_route_level("sword", 5)
Kit.check("已满级拒购/未解锁拒购(cap 闸+解锁闸——零副作用)〔锻造工坊批 V2〕",
not mp2.forge_buy_level("sword") and not mp2d.forge_buy_level("spear"))
# V3① 通道一:练满前一档(长剑 cap)→ 长枪/法杖派生解锁(零收费);轻甲未满→甲线仍锁
Kit.check("通道一:长剑练满→长枪/法杖派生解锁;轻甲未练满→重甲/魔法护甲仍锁〔锻造工坊批 V3〕",
mp2.forge_route_unlocked("spear") and mp2.forge_route_unlocked("staff")
and not mp2.forge_route_unlocked("heavy") and not mp2.forge_route_unlocked("ward"))
Kit.check("派生解锁后长枪可购(首购 20——余额 50−20)〔锻造工坊批 V3〕",
mp2.forge_buy_level("spear") and sv2.forge_route_level("spear") == 1
and sv2.keystone_balance() == 30)
# V3② 通道二:一次性解锁价直购(未练满也可买——两通道互不影响)
var sv3 = _meta_new_save(_META_SMOKE_PATHS[15])
var mp3 = load("res://core/meta_progression.gd").new(_db, sv3)
sv3.add_keystone(500)
Kit.check("通道二:魔法护甲一次性解锁价 200 直购开(轻甲未练满也可买)〔锻造工坊批 V3〕",
not mp3.forge_route_unlocked("ward") and mp3.forge_buy_unlock("ward")
and sv3.keystone_balance() == 300 and mp3.forge_route_unlocked("ward"))
# V3③ 直购后练满不重复收(练满通道=派生零收费;已解锁再直购=拒绝)
sv3.set_forge_route_level("light", 5)
Kit.check("直购后练满不重复收(已解锁=拒绝零收费——两通道互不影响)〔锻造工坊批 V3〕",
mp3.forge_route_unlocked("ward") and not mp3.forge_buy_unlock("ward")
and sv3.keystone_balance() == 300)
# V3④ 不设串联门槛(法杖直购开——长枪零状态不阻;dry 档充值走直购)
sv2d.add_keystone(500)
Kit.check("不设串联门槛(法杖直购开——长枪零状态不阻)〔锻造工坊批 V3〕",
mp2d.forge_buy_unlock("staff") and mp2d.forge_route_unlocked("staff")
and sv2d.forge_route_level("spear") == 0 and not mp2d.forge_route_unlocked("spear"))
# V4:任一购买/解锁只扣钥石——GP 侧零触碰(meta 域无 GP 键=域界扫描同款;此处钉余额差=当级价)
var ks4: int = sv3.keystone_balance()
Kit.check("两币分工:购买只扣钥石(余额差=当级价 20——GP 零触碰、无材料计数)〔锻造工坊批 V4〕",
mp3.forge_buy_level("ward") and sv3.keystone_balance() == ks4 - 20
and sv3.forge_route_level("ward") == 1)
# V6①:营地切换=写存档域+免费(余额零动);旧路线练度保留(只不生效)
var ks6: int = sv3.keystone_balance()
Kit.check("选中切换免费(余额零动——写存档域即盘)〔锻造工坊批 V6〕",
mp3.forge_select_armor("ward") and sv3.forge_armor() == "ward"
and sv3.keystone_balance() == ks6)
Kit.check("切换后旧路线练度保留(轻甲 5 级不丢——回家待命只不生效)〔锻造工坊批 V6〕",
sv3.forge_route_level("light") == 5)
# V6②:未解锁路线拒选零副作用(dry 档:长枪未解锁)
Kit.check("未解锁路线拒选零副作用〔锻造工坊批 V6〕",
not mp2d.forge_select_weapon("spear") and sv2d.forge_weapon() == "sword")
# V6③:类型轴读数随选中走(切换后读 ward;未解锁选中态防御回退默认值)
Kit.check("类型轴当前生效值随选中走(ward;未解锁档回退默认 normal/light)〔锻造工坊批 V5/V6〕",
mp3.hero_armor_type() == "ward" and mp2d.hero_attack_type() == "normal"
and mp2d.hero_armor_type() == "light")
# ---- T4 注入接线(V5 后半 · V6 锁死 · V7 公式对拍 · V10 树零攻防复核)----
# V5:build_adventure_params 注入默认值(新档=树钥匙未点亮)+点亮后读存档选中覆盖
var sv4 = _meta_new_save(_META_SMOKE_PATHS[16])
var mp4 = load("res://core/meta_progression.gd").new(_db, sv4)
var bp_params: Dictionary = load("res://presentation/main.gd").build_adventure_params(sv4, mp4)
Kit.check("build_adventure_params 注入默认值 normal/light(树钥匙未点亮——新档初始持有态)〔锻造工坊批 V5〕",
String(bp_params.get("attack_type", "")) == "normal"
and String(bp_params.get("armor_type", "")) == "light")
sv4.add_keystone(1000)
Kit.check("长枪直购解锁+首购+选中(注入面夹具——200+20 钥石)",
mp4.forge_buy_unlock("spear") and mp4.forge_buy_level("spear") and mp4.forge_select_weapon("spear"))
var bp_sel: Dictionary = load("res://presentation/main.gd").build_adventure_params(sv4, mp4)
Kit.check("build_adventure_params 读存档选中覆盖(选中长枪→pierce)〔锻造工坊批 V5〕",
String(bp_sel.get("attack_type", "")) == "pierce"
and String(bp_sel.get("armor_type", "")) == "light")
# V7:hero_override 公式对拍=树 flat+锻造路线加值(旧路线练度待命不生效;切回即生效)
Kit.check("hero_override=锻造路线加值(选中长枪 Lv1→atk +1;甲 0 级→AC +0;树零分量;无 hit 键)〔锻造工坊批 V7〕",
int(mp4.hero_override().get("atk", -1)) == 1
and int(mp4.hero_override().get("max_hp", -1)) == 0
and int(mp4.hero_override().get("armor_class", -1)) == 0
and not mp4.hero_override().has("hit"))
sv4.set_forge_route_level("sword", 3) # 长剑练度待命(未选中)
Kit.check("换当前武器→攻击加值随切换(长剑 3 级待命不生效——只认选中)〔锻造工坊批 V7〕",
int(mp4.hero_override().get("atk", -1)) == 1)
Kit.check("切回长剑→加值=3 级量(旧路线练度保留且生效)〔锻造工坊批 V7〕",
mp4.forge_select_weapon("sword") and int(mp4.hero_override().get("atk", -1)) == 3)
# V7 三层 AC:重甲(基础 12)+路线 Lv1 加值+局内 0——经参数面起冒险断 runner 生效值
var sv5 = _meta_new_save(_META_SMOKE_PATHS[17])
var mp5 = load("res://core/meta_progression.gd").new(_db, sv5)
sv5.add_keystone(1000)
Kit.check("重甲直购解锁+首购+选中(三层 AC 夹具)",
mp5.forge_buy_unlock("heavy") and mp5.forge_buy_level("heavy") and mp5.forge_select_armor("heavy"))
var adr5 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(77))
var p5: Dictionary = load("res://presentation/main.gd").build_adventure_params(sv5, mp5)
Kit.check("注入参数冒险启动(两轴+override 同面)", adr5.start_adventure(77, p5))
var r5 = adr5.runner()
Kit.check("入战 AC 三层:甲型基础 12(重甲)+路线加值 1+局内 0〔锻造工坊批 V7〕",
int(r5.effective_ac()) == 12 + 1)
Kit.check("参数面快照入 runner(attack_type=normal/armor_type=heavy)〔锻造工坊批 V6〕",
adr5.hero_attack_type == "normal" and adr5.hero_armor_type == "heavy")
# V6 锁死:开冒险后改存档选中→参数面/注入值不变(快照冻结;light=初始持有已解锁可切)
Kit.check("冒险开始后锁死:改存档选中不影响参数面快照与生效值(契约 8)〔锻造工坊批 V6〕",
mp5.forge_select_armor("light") and adr5.hero_armor_type == "heavy"
and int(r5.effective_ac()) == 12 + 1)
# V5/V7 末端:enc 两轴直达 spawn 英雄快照(hero_attack 行携带选中类型键)
var run_ax = _run_battle(1, 0, {"attack_type": "pierce", "armor_type": "ward"})
var pierce_rows := 0
for row in run_ax.stream:
if row.get("source_class", "") == "hero_attack" and row.has("pierce"):
pierce_rows += 1
Kit.check("enc 两轴直达 spawn 英雄快照(hero_attack 行携带 pierce 键——注入链末端)〔锻造工坊批 V5〕",
pierce_rows >= 1)
# spawn 显式注入面(三参旧形态零注入已在上方覆盖——此处钉显式值)
var uc2 = load("res://core/unit_container.gd").new(_db)
Kit.check("spawn 显式两轴注入(magic/heavy 直入英雄快照)〔锻造工坊批 V5〕",
uc2.spawn(1, 0, int(_db.hero.max_hp), [], -1, -1, [], "magic", "heavy")
and String(uc2.hero_unit().get("attack_type", "")) == "magic"
and String(uc2.hero_unit().get("armor_type", "")) == "heavy")
# V10 复核:树表 12 行 effects 键集无 atk/armor_class(契约 1——树零攻防来源)
var tree_atk_ac := false
for trow in _db.tree_rows():
var eff: Dictionary = trow.get("effects", {})
if eff.has("atk") or eff.has("armor_class"):
tree_atk_ac = true
Kit.check("树零攻防来源复核(12 行 effects 键集无 atk/armor_class——契约 1)〔锻造工坊批 V10〕",
not tree_atk_ac)
# ---- 天赋树冒烟(v0.8 12 节点:schema/购买面/Fork 一节点双态/彩蛋特化)----
func _smoke_tree() -> void:
print("--- 天赋树(12 节点 · 购买面 · Fork 双态 · 彩蛋觉醒)---")
# 〔V1〕结构:12 行 + schema 零违例
Kit.check("meta_tree 已加载(12 节点=脊柱 2〔生命/命中淬炼一线一节点 cap4〕+锻造 2〔钥匙+彩蛋——彩蛋含在锻造 2 内非第 13 行〕+钥石 2〔秘宝匣/刷新升级〕+同调 6〔socket 3+Fork 3 双态〕——v0.8 终态)〔天赋树批 V1〕",
_db.meta_tree != null and (_db.meta_tree.rows as Array).size() == 12)
Kit.check("MetaTreeTable.validate 零违例(脉/类型/币词表出域即拒/Fork 三组每组恰一且持 a/b 双效果键/彩蛋 ≤3/effects 键封闭/零 archetype)",
_db.meta_tree.validate().is_empty())
# 彩蛋行硬要求(契约 9 红线落点):effects 键集 ⊆ {max_hp, hit}
var ley: Dictionary = _db.meta_tree.row_by_id("easter_ley")
var ley_ok: bool = not ley.is_empty()
for k in (ley.get("effects", {}) as Dictionary).keys():
if not (String(k) == "max_hp" or String(k) == "hit"):
ley_ok = false
Kit.check("easter_ley effects 键集 ⊆ {max_hp, hit}(攻/护分量已摘除——契约 9)〔天赋树批 V8 落点〕",
ley_ok)
# V1/V4 反例:出域行逐类构造被拒(复制行副本塞词表外值)
var t2 = load("res://data/schemas/meta_tree_table.gd").new()
var bad_vein: Dictionary = (_db.meta_tree.row_by_id("sp_hp") as Dictionary).duplicate(true)
bad_vein["node_id"] = "bad_vein"
bad_vein["vein"] = "underworld"
t2.rows = [bad_vein]
Kit.check("schema 反例:vein 出域被拒", not t2.validate().is_empty())
var bad_fork: Dictionary = (_db.meta_tree.row_by_id("fork_posture") as Dictionary).duplicate(true)
bad_fork["node_id"] = "bad_fork"
bad_fork["fork_group"] = "fg_mastery"
t2.rows = [bad_fork]
Kit.check("schema 反例:fork_group 出域(fg_mastery 已退役组)被拒", not t2.validate().is_empty())
var bad_arch: Dictionary = (_db.meta_tree.row_by_id("sp_hp") as Dictionary).duplicate(true)
bad_arch["node_id"] = "bad_arch"
bad_arch["effects"] = {"archetype": 1}
t2.rows = [bad_arch]
Kit.check("schema 反例:effects.archetype 加载致命(🔴 层级纪律护栏面)〔天赋树批 V4〕",
not t2.validate().is_empty())
var bad_dangling: Dictionary = (_db.meta_tree.row_by_id("sp_hp") as Dictionary).duplicate(true)
bad_dangling["requires"] = ["ghost_node"]
t2.rows = [bad_dangling]
Kit.check("schema 反例:requires 悬空被拒", not t2.validate().is_empty())
# 购买面(V2 主干)
var sv = _meta_new_save(_META_SMOKE_PATHS[8])
var mp = load("res://core/meta_progression.gd").new(_db, sv)
sv.add_keystone(1000)
Kit.check("树购买受理(脊柱生命 L1——树全钥石单币)",
mp.tree_buy("sp_hp") and sv.tree_level("sp_hp") == 1)
Kit.check("行门槛:第 2 级不可购(总等级 1 < 4——一线一节点主语,买第 N 级需全树总投入 ≥4×(N−1))〔天赋树批 V2〕",
not mp.tree_can_buy("sp_hp"))
Kit.check("命中 L1/锻造钥匙(free 无前置=初始零购零币可点亮)/同调位 1(根节点无前置)受理(总等级 1→4)",
mp.tree_buy("sp_hit") and mp.tree_buy("forge_workshop") and mp.tree_buy("att_socket1"))
Kit.check("锻造工坊钥匙零币(free)", int(_db.tree_node("forge_workshop").get("price", -1)) == 0)
Kit.check("行门槛恰达开放(总等级 4 ≥ 4×1)", mp.tree_can_buy("sp_hp"))
Kit.check("第 2 级购买受理(门槛恰达当刻开放)",
mp.tree_buy("sp_hp") and sv.tree_level("sp_hp") == 2)
# Fork 一节点双态(V9)
sv.add_keystone(100)
Kit.check("Fork 购买=解锁+默认 A 态(一节点双态——单行承载)",
mp.tree_buy_fork("fork_posture") and sv.tree_level("fork_posture") == 1 and sv.fork_side("fg_posture") == "a")
Kit.check("已解锁 Fork 重复购买拒绝(cap 1)", not mp.tree_can_buy("fork_posture"))
Kit.check("同态重复切换零操作(false)", not mp.tree_toggle_fork("fg_posture", "a"))
Kit.check("未解锁 Fork 切换拒绝(解锁唯一入口=购买面)", not mp.tree_toggle_fork("fg_sense", "b"))
var bal_pre_toggle: int = sv.keystone_balance()
Kit.check("零币切态 b(购买解锁后 A/B 可切)",
mp.tree_toggle_fork("fg_posture", "b") and sv.fork_side("fg_posture") == "b")
Kit.check("切态零消费(钥石余额差分不动)", int(sv.keystone_balance()) == bal_pre_toggle)
Kit.check("规则键投影=激活态效果键(B 态稳健拓荒——tree_rule_keys 随切态换键集)",
(mp.tree_rule_keys() as Array).has("posture_bulwark") and not (mp.tree_rule_keys() as Array).has("posture_first_strike"))
Kit.check("切回 A 态键集随换(六键词表在册——V11 校验面归单元段)",
mp.tree_toggle_fork("fg_posture", "a") and (mp.tree_rule_keys() as Array).has("posture_first_strike"))
# 彩蛋特化觉醒(V8)
Kit.check("彩蛋不经购买(手动购亮=红——觉醒唯一入口=特化判定)", not mp.tree_can_buy("easter_ley"))
Kit.check("觉醒前置未满足不亮(两线未拉满)", sv.tree_level("easter_ley") == 0)
sv.set_tree_level("sp_hp", 4)
sv.set_tree_level("sp_hit", 4)
sv.set_tree_level("att_socket2", 1)
mp.tree_buy("att_socket3") # 经购买口触发 _check_easter_egg
Kit.check("两线拉满+同调位全解锁 → 地脉共鸣自动点亮(零币特化判定)", sv.tree_level("easter_ley") == 1)
var bal_now: int = sv.keystone_balance()
mp.tree_buy("sp_hp")
Kit.check("觉醒零消费(钥石余额不动)", int(sv.keystone_balance()) == bal_now)
_meta_smoke_cleanup()
# ---- 天赋树六键行为冒烟(V10:开局姿态窗/危险直觉限次/财富之道聚散/守侧失误补偿;
# 引擎直构 + 编排授予段直注(_grant_seg._judge_seal=结算授予段封印判定单口))----
func _smoke_tree_rules() -> void:
print("--- 天赋树六键行为(V10:窗/限次/聚散/守侧)---")
# 开局姿态组:每关首轮窗(battle_index 事实——引擎直构 + 自建 ctx)
var fx = load("res://core/effect_engine.gd").new(null)
fx.rebuild([], [], ["posture_first_strike", "posture_bulwark"])
var ctx_p := EffectContext.new()
Kit.check("先手压制:每关首战首轮窗命中(battle_index=1 且 round=1 → grant_ap queue)〔天赋树批 V10〕",
(fx.dispatch(ctx_p, "round_start", {"battle_index": 1, "round_index": 1}, Callable()) as Array).size() == 1)
Kit.check("先手压制:首轮窗外地不触发(第二轮/第二关首战首轮——「每战首轮」旧语义换代件)",
(fx.dispatch(ctx_p, "round_start", {"battle_index": 1, "round_index": 2}, Callable()) as Array).is_empty()
and (fx.dispatch(ctx_p, "round_start", {"battle_index": 2, "round_index": 1}, Callable()) as Array).is_empty())
Kit.check("稳健拓荒:首轮窗内受击出盾、窗外不出(shield damage_taken 同窗条件)",
(fx.dispatch(ctx_p, "damage_taken", {"battle_index": 1, "round_index": 1}, Callable()) as Array).size() == 1
and (fx.dispatch(ctx_p, "damage_taken", {"battle_index": 2, "round_index": 1}, Callable()) as Array).is_empty())
# 危险直觉·锋:限次闸(引擎直构——mine_hit 挂点现成;限次计数住 level 桶,跨战持久=共享同一字典引用)
var fz = load("res://core/effect_engine.gd").new(null)
fz.rebuild([], [], ["sense_sharp"])
var lvl_shared := {} # 同一关的 level 桶(两编排器共享引用 = 跨战持久,替代旧副本搬运)
var ctx_fz := EffectContext.new()
ctx_fz.level_bucket = lvl_shared
var got_mult := false
var got_add := false
for h in (fz.dispatch(ctx_fz, "mine_hit", {}, Callable()) as Array):
if String(h.get("kind", "")) == "damage_mult":
got_mult = true
if String(h.get("kind", "")) == "counter_add":
got_add = true
Kit.check("锋:踩雷减半命中+限次记账(counter 0<2 闸开——damage_mult+counter_add 双行同预评估态)",
got_mult and got_add)
EffectEngine.counter_add(ctx_fz, "lvl_tree_sense_sharp_used", 2) # 直调置数(V10 注入口径——置 level 桶计数后走事件路径)
Kit.check("锋:限次耗尽全额(counter 2 不<2 闸关——每关限次 provisional 2)〔无状态化 V5〕",
(fz.dispatch(ctx_fz, "mine_hit", {}, Callable()) as Array).is_empty())
var fz2 = load("res://core/effect_engine.gd").new(null)
fz2.rebuild([], [], ["sense_sharp"])
var ctx_fz2 := EffectContext.new()
ctx_fz2.level_bucket = lvl_shared # 挂同一 level 桶引用(新形态跨战持久:无 export/seed,同字典即持久)
Kit.check("锋:限次跨战持久(level 桶共享引用——替代旧副本搬运链)〔无状态化 V4〕",
(fz2.dispatch(ctx_fz2, "mine_hit", {}, Callable()) as Array).is_empty())
# 守侧五场景(契约 12):编排 rig——begin_battle 携 sense_guard,授予段 _grant_seg._judge_seal 直注
var orch = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777001))
Kit.check("守侧 rig:战斗编排启动(tree_rules=sense_guard 携带)",
orch.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40,
"hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {},
"feats": [], "tree_rules": ["sense_guard"], "battle_index": 1,
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {},
"voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}))
var board = orch.minesweeper.current_board()
var safe := Vector2i(-1, -1)
for c in board.all_cells():
if not board.is_mine(c):
safe = c
break
Kit.check("守侧 rig:安全格定位(非危险格=标错负样本)", safe.x >= 0)
Kit.check("守侧 rig:首轮后 queue 干净(开局 posture queue 已兑现)",
int(orch.action_economy.pending_ap_delta()) == 0)
# 场景①:当回合新封印、结算判标错且封印仍在场 → queue +1(本轮 AP 零变化=queue 承载、下轮 notify_round_start 兑现)
orch._grant_seg._judge_seal({"cell": safe, "hit": false}, 1)
Kit.check("守①:当回合标错且封印在场 → 下轮 AP+1(queue 排队——本轮 AP 逐位零变化)",
int(orch.action_economy.pending_ap_delta()) == 1)
# 场景③④:之前回合标错重判(封印在场状态)与同格重复——去重闸挡回,最多返还一次
orch._grant_seg._judge_seal({"cell": safe, "hit": false}, 2)
orch._grant_seg._judge_seal({"cell": safe, "hit": false}, 3)
Kit.check("守③④:旧封印重判零触发+同格最多一次(_seal_miss_seen 去重闸=「当回合标错动作」判别器)",
int(orch.action_economy.pending_ap_delta()) == 1)
# 场景②负样本:封印命中(hit=true 正向兑付路径)零触发
orch._grant_seg._judge_seal({"cell": safe, "hit": true}, 4)
Kit.check("守②负样本:标中/被挖封印零触发(标错事件只在 miss 分支发射——发射面过滤承载)",
int(orch.action_economy.pending_ap_delta()) == 1)
# 场景⑤:末轮判出→无下一轮=queue 不兑现(编排器每战新建——队列实例随战亡,合法作废态)
var orch2 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777002))
Kit.check("守⑤ rig:次战编排启动(新战新队列实例)",
orch2.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40,
"hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {},
"feats": [], "tree_rules": ["sense_guard"], "battle_index": 1,
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {},
"voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}))
Kit.check("守⑤:末轮判出不返(queue 只在 notify_round_start 兑现、战亡即弃——无跨战泄漏)",
int(orch2.action_economy.pending_ap_delta()) == 0)
# 树限次按关清零改由 LevelRunner.level_counters 随 new 承载(旧硬编码名单冒烟断言随函数删除——换代同生死;新语义由 V4/V5 段承载)
# 财富之道·散:商品标价折价(level_runner 计价单点)
var lr = load("res://core/level_runner.gd").new(_db, _fresh_rng(777004))
lr.set_tree_rules([])
Kit.check("散未激活:商品标价原价(无 economy_spend 键)", int(lr._spend_discounted(100)) == 100)
lr.set_tree_rules(["economy_spend"])
Kit.check("散激活:商品购买价 ×80%(provisional 折率——floor 偏向玩家、下限 1;刷新价不折=另一计价点零触碰)",
int(lr._spend_discounted(100)) == 80 and int(lr._spend_discounted(1)) == 1)
# 财富之道·聚:战斗掉落乘算·入账前(编排 _roll_gp_drop——同种子配对对拍 1.5×)
var orch3 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777005))
Kit.check("聚 rig:基线编排启动(无增益侧)",
orch3.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40,
"hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {},
"feats": [], "tree_rules": [], "battle_index": 1,
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {},
"voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}))
orch3.encounter["tree_rules"] = []
var gp_base: int = orch3._roll_gp_drop()
var orch4 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777005))
Kit.check("聚 rig:增益编排启动(tree_rules=economy_hoard)",
orch4.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40,
"hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {},
"feats": [], "tree_rules": ["economy_hoard"], "battle_index": 1,
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {},
"voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}))
orch4.encounter["tree_rules"] = ["economy_hoard"]
var gp_hoard: int = orch4._roll_gp_drop()
Kit.check("聚:战斗掉落乘算 ×1.5(provisional——同种子配对 %d→%d,入账前=结算框显示实得)",
gp_hoard == int(round(float(gp_base) * 1.5)) and gp_hoard >= gp_base)
_meta_smoke_cleanup()
@@ -0,0 +1,504 @@
# tests/unit/core/save_manager_domain_test.gd — 存档域单元测试宿主
# 断言面 = 两套(批 B 断言迁移自 smoke_all.gd,G10 施工组;两套按源编排序迁入):
# SaveManager 存档读写/迁移/兜底(读写往返/新档合法/余额拒花/高版本拒读禁写/损坏兜底/
# 迁移链/域界干净/摘要 cap/断点域七字段/旧档迁移族/收束链入账)
# SaveManager 三存档位承载(三槽往返/位间隔离/删档/新档白板/高版本拒读恢复/窥视三态/旧单档迁移)
# 协议照单元侧惯例:extends SceneTree+Kit.check 主通道;--script 模式无 autoload,
# ConfigDB 显式实例化构造注入(v2 断点续玩受理与收束链夹具消费 _db);表加载失败不中止
# (沿被迁断言面的源语义——诊断在此展开,红由具体断言落,承 G2–G5 同款)。
# 测试文件统一 user://smoke_save_*.json 隔离,段首清场+段尾清理——绝不碰 user://save.json(红线)。
# 分宿主注记:源 `_smoke_save` ⑨ 摘要集成块(level_autoplay 真局驱动×落盘,基点 :6807–6831)
# 经主会话裁决三改判集成测试——不随本宿主,见迁移账本转办清单。
# 运行:godot --headless --path game --script res://tests/unit/core/save_manager_domain_test.gd
extends SceneTree
const Kit := preload("res://tests/unit/test_kit.gd")
const ConfigDBScript := preload("res://core/config_db.gd")
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("[save_manager_domain_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_save()
_smoke_save_slots()
quit(Kit.finish("save_manager_domain_test"))
# ---- 隔离夹具(smoke_all.gd helper 的宿主自持私有副本——源文件收尾整体删除,宿主自给自足)----
# 冒烟隔离用全新 RngService(同 master 种子——回放确定性纪律)
func _fresh_rng(seed_val: int):
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(seed_val)
return rng
# ---- 存档序列化冒烟(SaveManager——读写往返/新档合法/余额拒花/锻造出域拒/高版本拒读禁写/
# 损坏兜底/迁移链/域界干净/摘要 cap/摘要集成/生涯终身计数域 v6)。测试文件统一 user://smoke_save_*.json 隔离,
# 段首清场+段尾清理——绝不碰 user://save.json(红线)。
func _smoke_save() -> void:
print("--- SaveManager 存档读写/迁移/兜底 ---")
var sm_script = load("res://core/save_manager.gd")
var paths := ["user://smoke_save_roundtrip.json", "user://smoke_save_fresh.json",
"user://smoke_save_spend.json", "user://smoke_save_highver.json",
"user://smoke_save_corrupt.json", "user://smoke_save_migrate.json",
"user://smoke_save_domain.json", "user://smoke_save_cap.json",
"user://smoke_save_integration.json", "user://smoke_save_breakpoint.json",
"user://smoke_save_v2compat.json", "user://smoke_save_v5upgrade.json",
"user://smoke_save_v5legacy.json", "user://smoke_save_v16discard.json",
"user://smoke_save_forge_v6legacy.json"]
var stale: Array = []
for p in paths:
stale.append(p)
stale.append(p + ".tmp")
stale.append(p + ".corrupt.bak")
for p in stale:
if FileAccess.file_exists(p):
DirAccess.remove_absolute(p)
# ① 往返:全访问器写非默认值 → write → 新实例 load → 逐域逐值相等
var w = sm_script.new(paths[0])
w.add_keystone(7)
w.set_tree_level("sp_hp", 2)
w.set_tree_level("sp_hit", 1)
w.set_fork_side("fg_posture", "b") # 树域写入
w.set_forge_route_level("spear", 3) # 锻造工坊域写入(等级+直购解锁+选中标记)
w.add_forge_unlock("spear")
w.set_forge_weapon("spear")
w.set_forge_armor("ward")
w.unlock_item("probe_compass")
w.apply_adventure_summary({"result": "failed", "cleared": false, "levels_cleared": 2,
"stopped_at": 3, "keystone_total": 3, "master_seed": 99})
Kit.check("apply 原语入账(钥石 7+3、通关次数 1/1/0)",
w.keystone_balance() == 10 and w.clear_count(1) == 1 and w.clear_count(2) == 1 and w.clear_count(3) == 0)
Kit.check("首杀标记随通关关卡置位(1/2 有、3 无)",
w.boss_first_kill_marked(1) and w.boss_first_kill_marked(2) and not w.boss_first_kill_marked(3))
Kit.check("生涯终身计数随 apply 自增恰一(中止局:总局数 1、通关局数 0——迭代 18 卡④)",
w.adventure_total() == 1 and w.adventure_cleared() == 0)
Kit.check("写盘成功", w.write_to_disk())
var r = sm_script.new(paths[0])
Kit.check("读回成功", r.load_from_disk())
Kit.check("钥石余额往返相等(10)", r.keystone_balance() == 10)
Kit.check("树域往返相等(tree_nodes/fork_sides——迭代 21 换代承载)",
r.tree_level("sp_hp") == 2 and r.tree_level("sp_hit") == 1 and r.fork_side("fg_posture") == "b")
Kit.check("锻造工坊域往返相等(levels/unlocked/weapon/armor——迭代 23 T5)〔锻造工坊批 V8〕",
r.forge_route_level("spear") == 3 and r.forge_route_level("sword") == 0
and r.forge_unlocked_purchases() == ["spear"]
and r.forge_weapon() == "spear" and r.forge_armor() == "ward")
Kit.check("道具解锁往返相等", r.item_unlocked("probe_compass") and not r.item_unlocked("rope"))
Kit.check("通关次数表往返相等", r.clear_count(1) == 1 and r.clear_count(2) == 1 and r.clear_count(3) == 0)
Kit.check("首杀标记往返相等",
r.boss_first_kill_marked(1) and r.boss_first_kill_marked(2) and not r.boss_first_kill_marked(3))
Kit.check("生涯终身计数往返相等(1/0)", r.adventure_total() == 1 and r.adventure_cleared() == 0)
var s0: Dictionary = r.recent_summaries()[0]
Kit.check("冒险摘要往返相等(1 条,seed 99)",
r.recent_summaries().size() == 1 and int(s0.get("master_seed", -1)) == 99 and int(s0.get("keystone_total", -1)) == 3)
# ② 新档合法:不存在路径 load → true + 全零默认值
var f2 = sm_script.new(paths[1])
Kit.check("新档合法态(load true + 全零默认)",
f2.load_from_disk() and f2.keystone_balance() == 0 and f2.tree_level("sp_hp") == 0
and not f2.item_unlocked("anything") and f2.clear_count(1) == 0 and f2.recent_summaries().is_empty())
Kit.check("新档生涯终身计数缺省全零(迭代 18 卡④两新域——白板存档完整可玩)",
f2.adventure_total() == 0 and f2.adventure_cleared() == 0)
# ③ spend 拒绝:余额不足 false 且零副作用(钥石只增不减——消费除外)
var f3 = sm_script.new(paths[2])
f3.add_keystone(5)
Kit.check("余额 5 花 8 拒绝且余额不动", not f3.spend_keystone(8) and f3.keystone_balance() == 5)
Kit.check("余额内消费成功(5→2)", f3.spend_keystone(3) and f3.keystone_balance() == 2)
# ⑤ 高版本拒读:load false、原文件字节不动、后续写盘拒绝且文件仍字节不动(契约 4)
var hv_text := "{\"version\": 99, \"meta\": {\"keystone_balance\": 1}}"
var fhv := FileAccess.open(paths[3], FileAccess.WRITE)
fhv.store_string(hv_text)
fhv.close()
var f5 = sm_script.new(paths[3])
Kit.check("高版本拒读(load false)", not f5.load_from_disk())
Kit.check("拒读后原文件字节不动", FileAccess.open(paths[3], FileAccess.READ).get_as_text() == hv_text)
Kit.check("拒读态写盘拒绝且文件仍字节不动",
not f5.write_to_disk() and FileAccess.open(paths[3], FileAccess.READ).get_as_text() == hv_text)
# ⑥ 损坏兜底:非 JSON 字节 → 默认数据 + .corrupt.bak(不静默丢档)
var junk := "not json {{{"
var fj := FileAccess.open(paths[4], FileAccess.WRITE)
fj.store_string(junk)
fj.close()
var f6 = sm_script.new(paths[4])
Kit.check("损坏档兜底(默认数据继续)",
f6.load_from_disk() and f6.keystone_balance() == 0 and f6.recent_summaries().is_empty())
Kit.check("坏档备份 .corrupt.bak 存在", FileAccess.file_exists(paths[4] + ".corrupt.bak"))
Kit.check("备份内容 = 原坏档字节",
FileAccess.open(paths[4] + ".corrupt.bak", FileAccess.READ).get_as_text() == junk)
# ⑦ 迁移链:register_migration 测试钩 → 手工 v0 旧结构档 → 链式升级 v1
var mig := func(d: Dictionary) -> Dictionary:
return {"version": 1, "meta": {"keystone_balance": int(d.get("keystone_old", 0))}}
var f7 = sm_script.new(paths[5])
f7.register_migration(0, mig)
var fm := FileAccess.open(paths[5], FileAccess.WRITE)
fm.store_string(JSON.stringify({"version": 0, "keystone_old": 12}))
fm.close()
Kit.check("v0 旧档经迁移链入账(keystone_old 12 → 余额 12)",
f7.load_from_disk() and f7.keystone_balance() == 12)
Kit.check("迁移链 0→1(测试钩)→7(生产) 写盘 version == 7(v7=锻造工坊域,迭代 23 T5——v6=生涯终身计数域迭代 18 卡④)",
f7.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[5], FileAccess.READ).get_as_text()).get("version", -1)) == 7)
# ⑧ 域界干净:meta 永久进度域不含局内资源键(行动点/RAP/GP/道具库存/生命账本不入档——契约 2;
# 扫描收窄到 meta 域——断点域=冒险进行中瞬态快照,生命/GP 属豁免面)
var disk_parsed = JSON.parse_string(FileAccess.open(paths[0], FileAccess.READ).get_as_text())
var meta_text := JSON.stringify((disk_parsed as Dictionary).get("meta", {}))
var banned := ["\"ap\"", "\"rap\"", "\"gp\"", "\"inventory\"", "\"hero_hp\""]
var leaked := []
for b in banned:
if meta_text.contains(b):
leaked.append(b)
Kit.check("域界干净——meta 域无局内资源键(发现 %s)" % str(leaked), leaked.is_empty())
# ⑩ 摘要 cap:12 份 → 保留 10、最新在前
var f10 = sm_script.new(paths[7])
for i in range(12):
f10.apply_adventure_summary({"result": "completed", "cleared": true, "levels_cleared": 0,
"stopped_at": 0, "keystone_total": 0, "master_seed": i})
var arr: Array = f10.recent_summaries()
Kit.check("摘要 cap 10(12 份 → 10 条)", arr.size() == 10)
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)——经主会话裁决三
# 改判集成测试,不随本宿主(迁移账本转办清单)。
# ⑪ 断点域:七字段写读往返逐位一致+附随件+值拷贝出户+清除+旧档迁移合法空态
var fbp = sm_script.new(paths[9])
Kit.check("断点:新档无断点(合法空态——「继续上局」隐藏)", not fbp.has_breakpoint())
fbp.set_breakpoint({
"level_index": 2, "node_id": 5, "hero_hp": 13,
"bonus": {"atk": 2, "max_hp": 5, "armor_class": 1},
"gp": 17, "shop_purchased": {"hp_small": 2, "atk_small": 1},
"template_pick": 3,
"master_seed": 424242, "rng_counter": 38, "keystone": 6,
"visited_ids": [0, 2, 5],
"aux_items": [{"item_id": "aux_scout", "source_class": "reward_node",
"origin_cell": [3, 4], "round_index": 2, "used": false}],
})
Kit.check("断点:写入+写盘成功", fbp.has_breakpoint() and fbp.write_to_disk())
var rb = sm_script.new(paths[9])
Kit.check("断点:读回在档", rb.load_from_disk() and rb.has_breakpoint())
var bp: Dictionary = rb.breakpoint_snapshot()
Kit.check("断点七字段往返:关卡序号/节点位/生命",
int(bp.get("level_index", -1)) == 2 and int(bp.get("node_id", -1)) == 5
and int(bp.get("hero_hp", -1)) == 13)
var bp_bonus: Dictionary = bp.get("bonus", {})
Kit.check("断点七字段往返:属性加成区",
int(bp_bonus.get("atk", -1)) == 2 and int(bp_bonus.get("max_hp", -1)) == 5
and int(bp_bonus.get("armor_class", -1)) == 1)
Kit.check("断点七字段往返:GP 钱包", int(bp.get("gp", -1)) == 17)
var bp_shop: Dictionary = bp.get("shop_purchased", {})
Kit.check("断点七字段往返:商店已购档",
int(bp_shop.get("hp_small", -1)) == 2 and int(bp_shop.get("atk_small", -1)) == 1)
Kit.check("断点七字段往返:已抽模板", int(bp.get("template_pick", -1)) == 3)
Kit.check("断点附随件往返:主种子/计数器/钥石累计",
int(bp.get("master_seed", -1)) == 424242 and int(bp.get("rng_counter", -1)) == 38
and int(bp.get("keystone", -1)) == 6)
var bp_visited: Array = bp.get("visited_ids", [])
Kit.check("断点附随件往返:已到节点序",
bp_visited.size() == 3 and int(bp_visited[0]) == 0 and int(bp_visited[2]) == 5)
var bp_treasure: Array = bp.get("aux_items", [])
Kit.check("断点附随件往返:道具库存",
bp_treasure.size() == 1 and String(bp_treasure[0].get("item_id", "")) == "aux_scout")
bp["hero_hp"] = 999
Kit.check("断点读面值拷贝(改读面不渗档)",
int((rb.breakpoint_snapshot() as Dictionary).get("hero_hp", -1)) == 13)
rb.clear_breakpoint()
Kit.check("断点:清除+写盘(三结局收束口径)", not rb.has_breakpoint() and rb.write_to_disk())
var rc = sm_script.new(paths[9])
Kit.check("断点:清除后读回无断点", rc.load_from_disk() and not rc.has_breakpoint())
var fo = sm_script.new(paths[9])
var fold := FileAccess.open(paths[9], FileAccess.WRITE)
fold.store_string("{\"version\": 1, \"meta\": {\"keystone_balance\": 4}}")
fold.close()
Kit.check("v1 旧档迁移 v7:断点域补空(合法空态,无断点域不炸)",
fo.load_from_disk() and fo.keystone_balance() == 4 and not fo.has_breakpoint())
Kit.check("v1 旧档迁移后写盘 version == 7(v1→v7 链式——锻造工坊域 2026-10-12)",
fo.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[9], FileAccess.READ).get_as_text()).get("version", -1)) == 7)
# ⑫ v2 旧档→v3 兼容(semver 向后兼容:旧断点零丢失、新代码可续玩)
var fv2 = sm_script.new(paths[10])
var fv2f := FileAccess.open(paths[10], FileAccess.WRITE)
fv2f.store_string(JSON.stringify({"version": 2, "meta": {"keystone_balance": 7},
"breakpoint": {"level_index": 1, "node_id": -1, "hero_hp": 40, "gp": 5,
"bonus": {"atk": 0, "max_hp": 0, "armor_class": 0}, "shop_purchased": {},
"template_pick": -1, "master_seed": 777, "rng_counter": 3, "keystone": 0,
"visited_ids": []}}))
fv2f.close()
Kit.check("v2 旧档迁移 v3:断点域与 meta 域原样保留(缺新附随件=合法空态)",
fv2.load_from_disk() and fv2.has_breakpoint() and fv2.keystone_balance() == 7)
Kit.check("v2 旧断点字段零丢失(向后兼容——breakpoint 内容不因 schema 升版重写)",
int((fv2.breakpoint_snapshot() as Dictionary).get("master_seed", 0)) == 777)
var adr_v2 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(777))
Kit.check("v2 旧断点续玩受理(缺 bonus 附随件走常规重建路径——g 裁 semver 口径)",
adr_v2.resume_adventure(fv2.breakpoint_snapshot(), {"hero_override": {}}))
# ⑬ 生涯终身计数域:v5 旧档升 v6 缺省补零 + 升档幂等 + 收束自增恰一
#(真实旧档形态=计数域两键不存在;升档只增不改——既有 meta 值零触碰,存档红线 S2 断言面)
var f13 = sm_script.new(paths[11])
var fv5u := FileAccess.open(paths[11], FileAccess.WRITE)
fv5u.store_string(JSON.stringify({"version": 5, "meta": {"keystone_balance": 9}}))
fv5u.close()
Kit.check("v5 旧档升 v7:计数域缺省补零且既有余额原样(迁移只增字段不改值——v5→v6→v7 链式)",
f13.load_from_disk() and f13.adventure_total() == 0 and f13.adventure_cleared() == 0
and f13.keystone_balance() == 9)
Kit.check("升档幂等:v7 档写盘再载零漂移(重复迁移零副作用——锻造工坊域同批补齐)",
f13.write_to_disk() and f13.load_from_disk()
and int(JSON.parse_string(FileAccess.open(paths[11], FileAccess.READ).get_as_text()).get("version", -1)) == 7
and f13.adventure_total() == 0 and f13.adventure_cleared() == 0)
var f13b = sm_script.new(paths[12])
f13b.load_from_disk()
f13b.apply_adventure_summary({"result": "completed", "cleared": true, "levels_cleared": 3,
"stopped_at": 0, "keystone_total": 0, "master_seed": 1})
Kit.check("收束自增恰一·通关局(三关全通 cleared=true:总局数+1 且通关局数+1)",
f13b.adventure_total() == 1 and f13b.adventure_cleared() == 1)
f13b.apply_adventure_summary({"result": "abandoned", "cleared": false, "levels_cleared": 1,
"stopped_at": 2, "keystone_total": 0, "master_seed": 2})
Kit.check("收束自增恰一·中止局(放弃局:仅总局数+1,通关局数不动)",
f13b.adventure_total() == 2 and f13b.adventure_cleared() == 1)
# ⑮ 旧档破坏性废弃(契约 4:换代前结构检测=树域废弃重置,禁迁移禁折算)〔V16〕
var f15 = sm_script.new(paths[13])
var fv16 := FileAccess.open(paths[13], FileAccess.WRITE)
fv16.store_string(JSON.stringify({"version": 6, "meta": {"keystone_balance": 33,
"forge_levels": {"max_hp": 3, "atk": 4, "armor_class": 2},
"tree_nodes": {"sp_hp_r1": 3, "forge_atk": 2, "fork_posture_a": 1},
"fork_sides": {"fg_posture": "a", "fg_mastery": "b"}}}))
fv16.close()
Kit.check("换代前结构存档:树域废弃重置(旧节点 id/fork 侧/forge_levels 旧域残留清退)、钥石余额零折算零变动",
f15.load_from_disk() and f15.keystone_balance() == 33
and f15.tree_nodes().is_empty() and f15.fork_sides().is_empty())
var mp16 = load("res://core/meta_progression.gd").new(_db, f15)
f15.add_keystone(50)
Kit.check("废弃重置后后续购买正常受理(无残留脏节点)",
mp16.tree_buy("sp_hp") and f15.tree_level("sp_hp") == 1)
# ⑯ 锻造工坊域 forge_routes(V8):v6 旧档缺键补齐=初始持有态+无特效域+域界干净
var f16 = sm_script.new(paths[14])
var fv6 := FileAccess.open(paths[14], FileAccess.WRITE)
fv6.store_string(JSON.stringify({"version": 6, "meta": {"keystone_balance": 21,
"tree_nodes": {"sp_hp": 1}, "tree_struct": 2, "adventure_total": 4, "adventure_cleared": 1}}))
fv6.close()
Kit.check("v6 旧档升 v7:既有域逐位不动(钥石/树/生涯计数零折算——迁移只增不改)〔锻造工坊批 V8〕",
f16.load_from_disk() and f16.keystone_balance() == 21
and f16.tree_level("sp_hp") == 1 and f16.adventure_total() == 4 and f16.adventure_cleared() == 1)
Kit.check("v6 旧档缺键补齐=初始持有态(六路线 0 级/直购清单空/选中=sword+light)〔锻造工坊批 V8〕",
f16.forge_route_level("sword") == 0 and f16.forge_route_level("spear") == 0
and f16.forge_route_level("light") == 0 and f16.forge_route_level("ward") == 0
and f16.forge_unlocked_purchases().is_empty()
and f16.forge_weapon() == "sword" and f16.forge_armor() == "light")
var fr_keys: Array = (f16.forge_routes_snapshot() as Dictionary).keys()
fr_keys.sort()
Kit.check("forge_routes 域键集恰四项(无特效域——满级特效本期不做,契约 7)〔锻造工坊批 V8/V11〕",
fr_keys == ["armor", "levels", "unlocked", "weapon"])
var lv_keys: Array = ((f16.forge_routes_snapshot() as Dictionary).get("levels", {}) as Dictionary).keys()
lv_keys.sort()
Kit.check("levels 六路线键集恰全(sword/spear/staff/light/heavy/ward)〔锻造工坊批 V8〕",
lv_keys == ["heavy", "light", "spear", "staff", "sword", "ward"])
# 值拷贝出户(改快照不渗档——本类契约)
var snap: Dictionary = f16.forge_routes_snapshot()
(snap.get("levels", {}) as Dictionary)["sword"] = 99
Kit.check("forge_routes_snapshot 值拷贝出户(改快照不渗档)〔锻造工坊批 V8〕",
f16.forge_route_level("sword") == 0)
# ⑭ 收束链入账(真实收束挂点全链恰一:主流程 adventure_finished →
# MetaProgression.settle_adventure → apply_adventure_summary 单口;两夹具经同链入账)
var mp13 = load("res://core/meta_progression.gd").new(_db, f13b)
mp13.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 2,
"stopped_at": 3, "keystone_total": 0, "master_seed": 3})
Kit.check("收束链自增·中止局(settle_adventure 真实挂点:总局数+1、通关局数不动)",
f13b.adventure_total() == 3 and f13b.adventure_cleared() == 1)
mp13.settle_adventure({"result": "completed", "cleared": true, "levels_cleared": 3,
"stopped_at": 0, "keystone_total": 0, "master_seed": 4})
Kit.check("收束链自增·通关局(三关全通:总局数+1 且通关局数+1——全链恰一)",
f13b.adventure_total() == 4 and f13b.adventure_cleared() == 2)
# 段尾清理(测试文件绝不残留)
for p in stale:
if FileAccess.file_exists(p):
DirAccess.remove_absolute(p)
# ---- 三存档位承载(迭代 33 卡①:三槽往返/新档/进档/删档/窥视三态/各槽迁移/旧单档迁移)----
# 测试文件统一 user://smoke_save_slot_%d.json 模板隔离(attach_slot_template 注入口——禁各自拼槽名),
# 段首清场+段尾清理——绝不碰 user://save.json 与真实槽 user://save_slot_N.json(红线)。
func _smoke_save_slots() -> void:
print("--- SaveManager 三存档位承载(迭代 33 卡①)---")
var sm_script = load("res://core/save_manager.gd")
var slot_tpl := "user://smoke_save_slot_%d.json"
var legacy_src := "user://smoke_save_legacy_src.json"
var stale: Array = []
for i in range(1, 4):
for suf in ["", ".tmp", ".corrupt.bak"]:
stale.append(slot_tpl % i + suf)
stale.append_array([legacy_src, legacy_src + ".migrated.bak"])
for p in stale:
if FileAccess.file_exists(p):
DirAccess.remove_absolute(p)
var t = sm_script.new()
t.attach_slot_template(slot_tpl)
# ① 三槽往返×3+位间隔离+进档(sv1/sv5/sv10)
for i in range(1, 4):
Kit.check("sv5 进档受理·槽 %d(激活+读盘)〔卡① sv5〕" % i, t.activate_slot(i))
t.apply_adventure_summary({"result": "completed", "cleared": i == 1, "levels_cleared": i,
"stopped_at": 0, "keystone_total": 100 * i, "master_seed": i})
Kit.check("sv3 激活态写盘受理·槽 %d〔卡① sv1〕" % i, t.write_to_disk())
var t2 = sm_script.new()
t2.attach_slot_template(slot_tpl)
for i in range(1, 4):
Kit.check("sv1 三槽往返一致·槽 %d(异值按位读回)〔卡① sv1〕" % i,
t2.activate_slot(i) and t2.keystone_balance() == 100 * i
and t2.adventure_total() == 1 and t2.adventure_cleared() == (1 if i == 1 else 0))
var slot2_before := FileAccess.get_file_as_bytes(slot_tpl % 2)
t.activate_slot(1)
t.add_keystone(7)
Kit.check("sv3 写盘受理·槽 1 加值后〔卡① sv1〕", t.write_to_disk())
Kit.check("sv1 位间隔离:改槽 1 槽 2 字节不动〔卡① sv1〕",
FileAccess.get_file_as_bytes(slot_tpl % 2) == slot2_before)
# ② 删档原语(sv6):删激活槽(指针连清除)→ 删非激活槽(指针保持+他槽字节不动)
Kit.check("sv5 切激活至槽 3(删激活槽夹具前置)〔卡① sv5〕", t.activate_slot(3))
Kit.check("sv6 删档受理·激活槽 3〔卡① sv6〕", t.delete_slot(3))
Kit.check("sv6 删后文件不在盘+无 .tmp 残留〔卡① sv6〕",
not FileAccess.file_exists(slot_tpl % 3) and not FileAccess.file_exists(slot_tpl % 3 + ".tmp"))
Kit.check("sv6 删激活槽激活指针连清除(该槽不再可写)〔卡① sv6〕",
t.active_slot() == 0 and not t.has_write_target())
var slot1_before := FileAccess.get_file_as_bytes(slot_tpl % 1)
t.activate_slot(1)
Kit.check("sv6 删非激活槽受理·槽 2〔卡① sv6〕", t.delete_slot(2))
Kit.check("sv6 删非激活槽:文件不在盘+激活指针保持〔卡① sv6〕",
not FileAccess.file_exists(slot_tpl % 2) and t.active_slot() == 1)
Kit.check("sv6 删槽 2 槽 1 字节不动(位间互隔离)〔卡① sv6〕",
FileAccess.get_file_as_bytes(slot_tpl % 1) == slot1_before)
# ③ 新档原语(sv4):非白板旧内容覆盖为全零可玩白板(判别锚:新档禁读旧档——变异可红),
# 创建即激活并落盘(生产路径只在空槽调用;本段预置旧内容是对原语白板保证的钉死,超生产面)
var fold := FileAccess.open(slot_tpl % 3, FileAccess.WRITE)
fold.store_string(JSON.stringify({"version": 7, "meta": {"keystone_balance": 999,
"adventure_total": 5, "adventure_cleared": 2, "tree_nodes": {}, "fork_sides": {}, "tree_struct": 2}}))
fold.close()
Kit.check("sv4 新档受理·槽 3(创建即激活)〔卡① sv4〕", t.create_slot(3))
Kit.check("sv4 新档=全零可玩合法白板态(旧内容 999/5 零带入——钥石/总局数/断点全零)〔卡① sv4〕",
t.active_slot() == 3 and t.keystone_balance() == 0 and not t.has_breakpoint()
and t.adventure_total() == 0)
var created: Dictionary = JSON.parse_string(FileAccess.get_file_as_string(slot_tpl % 3))
Kit.check("sv4 新档落盘=白板内容(盘面核验非 999 旧值)〔卡① sv4〕",
int((created.get("meta", {}) as Dictionary).get("keystone_balance", -1)) == 0)
# ④ 高版本拒读槽按槽成立+激活失败恢复切换前三态(sv9/sv10)
var fhv := FileAccess.open(slot_tpl % 2, FileAccess.WRITE)
fhv.store_string(JSON.stringify({"version": 99, "meta": {}}))
fhv.close()
var hv = sm_script.new()
hv.attach_slot_template(slot_tpl)
Kit.check("sv5 进档槽 1 读回(107)〔卡① sv10〕",
hv.activate_slot(1) and hv.keystone_balance() == 107)
var slot2_hv := FileAccess.get_file_as_bytes(slot_tpl % 2)
Kit.check("sv9 高版本槽进档拒绝(拒读禁写链按槽)〔卡① sv9〕", not hv.activate_slot(2))
Kit.check("sv10 激活失败恢复切换前三态(路径/激活号/数据归槽 1)〔卡① sv10〕",
hv.active_slot() == 1 and hv.keystone_balance() == 107)
hv.add_keystone(1)
Kit.check("sv10 恢复后写盘落激活槽 1(高版本槽 2 零误写)〔卡① sv10〕",
hv.write_to_disk() and FileAccess.get_file_as_bytes(slot_tpl % 2) == slot2_hv)
# ⑤ 迁移链各槽成立(sv9):三槽各喂 v5 旧档 → 各自激活读回迁移值
for i in range(1, 4):
var fm := FileAccess.open(slot_tpl % i, FileAccess.WRITE)
fm.store_string(JSON.stringify({"version": 5, "meta": {"keystone_balance": i * 11}}))
fm.close()
for i in range(1, 4):
var mi = sm_script.new()
mi.attach_slot_template(slot_tpl)
Kit.check("sv9 迁移链按槽成立·槽 %d(v5→v7 各槽独立+写盘 version=7)〔卡① sv9〕" % i,
mi.activate_slot(i) and mi.keystone_balance() == i * 11
and mi.write_to_disk())
# ⑥ 窥视三态零副作用(sv7/sv8):异值三槽逐槽三项+空槽零值+坏/高版本字节 mtime 双比对
var pv = sm_script.new()
pv.attach_slot_template(slot_tpl)
for i in range(1, 4):
var fp := FileAccess.open(slot_tpl % i, FileAccess.WRITE)
fp.store_string(JSON.stringify({"version": 7, "meta": {"keystone_balance": 200 * i,
"adventure_total": 5 * i, "adventure_cleared": i,
"tree_nodes": {}, "fork_sides": {}, "tree_struct": 2}}))
fp.close()
for i in range(1, 4):
var ps: Dictionary = pv.peek_slot_summary(i)
Kit.check("sv7 窥视三项逐槽正确·槽 %d〔卡① sv7〕" % i,
bool(ps.get("readable", false)) and int(ps.get("keystone", -1)) == 200 * i
and int(ps.get("cleared", -1)) == i and int(ps.get("total", -1)) == 5 * i)
Kit.check("sv6 删槽 3 备空槽窥视〔卡① sv6〕", pv.delete_slot(3))
var pe: Dictionary = pv.peek_slot_summary(3)
Kit.check("sv7 空槽窥视=零值合法态〔卡① sv7〕",
bool(pe.get("readable", false)) and int(pe.get("keystone", -1)) == 0
and int(pe.get("cleared", -1)) == 0 and int(pe.get("total", -1)) == 0)
var fbad := FileAccess.open(slot_tpl % 1, FileAccess.WRITE)
fbad.store_string("not json{{{")
fbad.close()
var bad_bytes := FileAccess.get_file_as_bytes(slot_tpl % 1)
var bad_mtime := FileAccess.get_modified_time(slot_tpl % 1)
var pb: Dictionary = pv.peek_slot_summary(1)
Kit.check("sv8 坏槽窥视=显式损坏标记〔卡① sv8〕", not bool(pb.get("readable", true)))
Kit.check("sv8 坏槽窥视零副作用(字节+mtime 双比对不变+零 .corrupt.bak 改名)〔卡① sv8〕",
FileAccess.get_file_as_bytes(slot_tpl % 1) == bad_bytes
and FileAccess.get_modified_time(slot_tpl % 1) == bad_mtime
and not FileAccess.file_exists(slot_tpl % 1 + ".corrupt.bak"))
var fhv2 := FileAccess.open(slot_tpl % 2, FileAccess.WRITE)
fhv2.store_string(JSON.stringify({"version": 99, "meta": {}}))
fhv2.close()
var hv_bytes := FileAccess.get_file_as_bytes(slot_tpl % 2)
var hv_mtime := FileAccess.get_modified_time(slot_tpl % 2)
var ph: Dictionary = pv.peek_slot_summary(2)
Kit.check("sv8 高版本槽窥视=不可读标记〔卡① sv8〕", not bool(ph.get("readable", true)))
Kit.check("sv8 高版本窥视零副作用(字节+mtime 不变+零改名)〔卡① sv8〕",
FileAccess.get_file_as_bytes(slot_tpl % 2) == hv_bytes
and FileAccess.get_modified_time(slot_tpl % 2) == hv_mtime)
# ⑦ 旧单档迁移(sv12):字节搬运+源改名留痕+幂等+他槽零触碰
var ml = sm_script.new()
ml.attach_slot_template(slot_tpl)
var fsrc := FileAccess.open(legacy_src, FileAccess.WRITE)
fsrc.store_string(JSON.stringify({"version": 7, "meta": {"keystone_balance": 1234}}))
fsrc.close()
DirAccess.remove_absolute(slot_tpl % 1)
ml._migrate_legacy_save(legacy_src, slot_tpl % 1, legacy_src + ".migrated.bak")
var migrated: Dictionary = JSON.parse_string(FileAccess.get_file_as_string(slot_tpl % 1))
Kit.check("sv12 旧档字节迁入槽 1(逐域相等——余额 1234)〔卡① sv12〕",
int((migrated.get("meta", {}) as Dictionary).get("keystone_balance", -1)) == 1234)
Kit.check("sv12 迁移后源改名留痕(.migrated.bak 在盘、原路径不在)〔卡① sv12〕",
not FileAccess.file_exists(legacy_src) and FileAccess.file_exists(legacy_src + ".migrated.bak"))
var s2_before := FileAccess.get_file_as_bytes(slot_tpl % 2)
var s3_before := FileAccess.get_file_as_bytes(slot_tpl % 3)
var fsrc2 := FileAccess.open(legacy_src, FileAccess.WRITE)
fsrc2.store_string(JSON.stringify({"version": 7, "meta": {"keystone_balance": 777}}))
fsrc2.close()
ml._migrate_legacy_save(legacy_src, slot_tpl % 1, legacy_src + ".migrated.bak")
var slot1_now: Dictionary = JSON.parse_string(FileAccess.get_file_as_string(slot_tpl % 1))
Kit.check("sv12 幂等:槽 1 已存在不重迁(预置 777 零覆盖、源保留)〔卡① sv12〕",
int((slot1_now.get("meta", {}) as Dictionary).get("keystone_balance", -1)) == 1234
and FileAccess.file_exists(legacy_src))
Kit.check("sv12 迁移他槽零触碰(槽 2/3 字节不动)〔卡① sv12〕",
FileAccess.get_file_as_bytes(slot_tpl % 2) == s2_before
and FileAccess.get_file_as_bytes(slot_tpl % 3) == s3_before)
# 段尾清理(测试文件绝不残留)
for p in stale:
if FileAccess.file_exists(p):
DirAccess.remove_absolute(p)
@@ -0,0 +1,53 @@
# tests/unit/data_config/rng_stream_boot_test.gd — 数据配置加载链与统一 RNG 流单元测试宿主
# 断言面 = 启动初始化套(批 B 断言迁移自 smoke_all.gd `_initialize`,G12 施工组):
# data/tables 加载链(全部表加载 + 跨表护栏)
# core/rng_service.gd 统一流(master 种子 42 黄金向量 / derive_at 纯函数恒等 /
# open_stream 取号序 / new_adventure 会话计数器重置)
# 协议照单元侧惯例:extends SceneTree+Kit.check 主通道;--script 模式无 autoload,
# ConfigDB/RngService 显式实例化构造注入(master 种子 42 沿源);
# 表加载失败不中止(沿被迁断言面的源语义——诊断在此展开,加载链的红由具体断言落)。
# 运行:godot --headless --path game --script res://tests/unit/data_config/rng_stream_boot_test.gd
extends SceneTree
const Kit := preload("res://tests/unit/test_kit.gd")
const ConfigDBScript := preload("res://core/config_db.gd")
func _init() -> void:
_smoke_initialize()
quit(Kit.finish("rng_stream_boot_test"))
# ---- 启动初始化断言面(宿主接线注记:源函数名 `_initialize` 是 MainLoop 虚拟入口,
# 单元宿主改由 `_init` 显式调用,断言逐字未动;源 :70 事件日志落盘条经主会话裁决一
# 改判集成测试——不随本宿主,见迁移账本转办清单)----
func _smoke_initialize() -> void:
print("=== Dungeon Sweeper 垂直切片冒烟 ===")
var db = ConfigDBScript.new()
var load_ok: bool = db.load_tables("res://data/tables", true)
Kit.check("全部表加载 + 校验通过(含跨表护栏)", load_ok)
if not load_ok:
for d in db.diagnostics():
if d.severity == "error":
print(" error: [%s] %s" % [d.category, d.message])
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(42)
# 统一流黄金向量(数据配置 AC-3:单盐沿用 board_gen 盐,
# scope 0-3 与 board_gen 向量逐位连续;Python 参考实现逐位交叉验证一致)
Kit.check("统一流向量 m=42 scope=0(=旧 board_gen scope0 连续性锚)", rng.derive_at(0) == 8672514112661456564)
Kit.check("统一流向量 m=42 scope=1", rng.derive_at(1) == 8834291164406037215)
Kit.check("统一流向量 m=42 scope=2", rng.derive_at(2) == 7714373262647006282)
Kit.check("统一流向量 m=42 scope=3", rng.derive_at(3) == 1280102845971471347)
Kit.check("统一流向量 m=42 scope=4", rng.derive_at(4) == 82585501446482399)
Kit.check("统一流向量 m=42 scope=5", rng.derive_at(5) == 8078924421474470922)
Kit.check("统一流向量 m=42 scope=6", rng.derive_at(6) == 6638052740430622424)
Kit.check("统一流向量 m=42 scope=7", rng.derive_at(7) == 8264196488417721294)
Kit.check("derive_at 纯函数恒等", rng.derive_at(0) == rng.derive_at(0))
# scope 取号语义:首个 open_stream = 全局 scope 0;计数器单调;用途标签不参与派生(只进事件日志)
var s0 = rng.open_stream("board_gen")
var s1 = rng.open_stream("encounter")
Kit.check("open_stream 全局计数器单调 +1 ×2", rng.counter() == 2)
Kit.check("首个取号 = scope 0(顺序确定)", s0.seed == rng.derive_at(0) and s1.seed == rng.derive_at(1))
rng.new_adventure(4242)
var s2 = rng.open_stream("gp_drop")
Kit.check("new_adventure 重置全局计数器", rng.counter() == 1 and s2.seed == rng.derive_at(0))
@@ -127,8 +127,10 @@ func _smoke_adventure_table() -> void:
int(_db.adventure.curio_value("heal", "excellent")) == 6 and int(_db.adventure.curio_value("ap", "rare")) == 1)
Kit.check("cap 占位在域(案②提前退役——99=实质无上限,实数归调参批)〔专长批 V8 形态面〕",
int(_db.adventure.curio_cap("gp")) == 99 and int(_db.adventure.curio_cap("attack")) == 99)
Kit.check("同调位读数源=树同调位节点点亮数(迭代 21 T4 槽位收编——null 档求值 0;谓词表已退役)〔天赋树批 V14〕",
Kit.check("同调位读数 null 档早退分支求值 0(迭代 21 T4 槽位收编——谓词表已退役)〔天赋树批 V14·核代码增补〕",
Attunement.slots(_db, null) == 0)
Kit.check("同调位读数源=树同调位节点点亮数(非空档——socket1 点亮 1 件计 1)〔天赋树批 V14·核代码增补〕",
Attunement.slots(_db, _TreeSaveMock.new()) == 1)
# 同调目录 12 件(V1/V2 断言落点;装配→战斗贯通与空装配基线的现行主语 = morning_order)
var trows: Array = _db.treasure_attunement_rows()
var tid_set := {}
@@ -306,6 +308,11 @@ func _smoke_ac_swap_guard() -> void:
wall_detail += " %s(%d)" % [eff_id, floor_v]
Kit.check("hero_defense 池 percent 行叠满不可触池底(墙最深%s > clamp_lo %d——单效果归零结构性不可达)" % [
wall_detail if wall_detail != "" else "(池内现无负向行)", int(_db.settlement.pool_clamp_lo)], wall_ok)
var hd_maps := 0
for eff_id in _db.settlement.effect_pool_map.keys():
if String(_db.settlement.effect_pool_map[eff_id]) == "hero_defense":
hd_maps += 1
Kit.check("hero_defense 池映射数==0(绊线钉——红=已加映射,去核上方护栏是否已同步激活)", hd_maps == 0)
# ③ 单位表 armor_class:域内校验(配置推导——正式投值归怪物/难度批,改表不断锚)
Kit.check("主角 armor_class 在域内(%d——暂定基准随表走)" % int(_db.hero.armor_class),
int(_db.hero.armor_class) >= 0 and int(_db.hero.armor_class) <= 30)
@@ -491,6 +498,8 @@ func _smoke_nine_types_gp_stream() -> void:
pos_all_in_pool and neg_all_zero)
Kit.check("gp 型两行(GP 袋 gp_gain 正向 weight 2 / 破财 gp_loss 负向 weight 0 不入池)", gp_pos == 2 and gp_neg == 0)
var rng = _fresh_rng(42)
for i in 3:
rng.derive_at(i)
Kit.check("derive_at 纯函数不推计数器(统一流全局计数器恒 0——并流后纯函数语义不变,gp_drop 专用向量随分盐作废、统一向量中央断言)",
rng.counter() == 0)
@@ -514,3 +523,9 @@ func _smoke_meta() -> void:
var brow: Dictionary = _db.meta_boss_keystone_row(li)
boss_ok = boss_ok and not brow.is_empty() and int(brow.get("first_kill", 0)) >= int(brow.get("repeat_kill", -1))
Kit.check("boss_keystone L1–L3 行连续可达且 first_kill ≥ repeat_kill 全行(首杀全量/重复小额)", boss_ok)
class _TreeSaveMock:
# 核代码增补:最小 save 形态——attunement.slots 只读 tree_level("att_socketN")
func tree_level(n: String) -> int:
return 1 if n == "att_socket1" else 0
@@ -0,0 +1,867 @@
# tests/unit/presentation/theme_projection_test.gd — 呈现投影与主题单元测试宿主
# 断言面 = 十套(批 B 断言迁移自 smoke_all.gd,G11 施工组;十套按源编排序迁入):
# 未开格类型微光(作用集恒等/九型归组封闭/零玩法写入——board_view 微光数据面)
# 战斗日志视图行格式化(行协议 9 类呈现层同步闸——真人试玩卡断 bug 回归)
# UI 全局主题(main_theme.tres 挂载/类型覆盖/滑轨两段对比/退役实现零残留/中文字体防御)
# UI 按钮五态矩阵(四工厂五态样式盒逐实例 override)
# UI 全局纹理过滤(工程 Nearest+平滑渐变件 LINEAR 白名单+光柱 alpha 结构)
# UI 残余 <4.5:1 文字位换墨(站桩徽标两族亮档+RAP 行深石墨对比度锚定)
# UI 元素细节余②③(BCard 图标槽明度差降档+商店居中+营地四栏等高源码锚)
# UI 诊断转储钮退役(玩家画面清零+浮窗可达——源码契约)
# 状态面板只读投影一致性(展示值逐项 == MetaProgression 访问器读数)
# 生涯页只读投影一致性(汇总段六类九项逐行 == SaveManager/MetaProgression 访问器读数+v5 旧档显示)
# 协议照单元侧惯例:extends SceneTree+Kit.check 主通道;--script 模式无 autoload,
# ConfigDB/RngService 显式实例化构造注入(统一流 master 种子 42 沿源);表加载失败不中止
# (沿被迁断言面的源语义——诊断在此展开,红由具体断言落,承 G2–G5 同款);全局 class_name
# (UiKit)裸用(--script 模式全局类在册,G3 先例)。
# 分宿主注记:源 `_smoke_playtest_logger`(挂真实结算流跨系统)经主会话裁决七改判集成测试,
# 不随本宿主——见迁移账本转办清单;`_smoke_global_theme` 依赖 ThemeDB 项目主题在座,
# 裁决四要求实跑验证(本批零测试运行,验证归装配验证轮)。
# 运行:godot --headless --path game --script res://tests/unit/presentation/theme_projection_test.gd
extends SceneTree
const Kit := preload("res://tests/unit/test_kit.gd")
const ConfigDBScript := preload("res://core/config_db.gd")
const AutoPlayer := preload("res://smoke/auto_player.gd") # 自动玩家策略单一实现(迁移期按现路径 preload——账本 §五夹具前提)
var _db
var _rng
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("[theme_projection_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])
_rng = load("res://core/rng_service.gd").new()
_rng.set_master_seed(42)
# 套执行序沿源编排序(_run_core_smoke 序 + _smoke_meta 薄壳尾序)
_smoke_type_glow()
_smoke_battle_log_view()
_smoke_global_theme()
_smoke_button_five_states()
_smoke_texture_filter()
_smoke_contrast_inks()
_smoke_element_details()
_smoke_diag_button_retired()
_status_panel_projection()
_career_overlay_projection()
quit(Kit.finish("theme_projection_test"))
# ---- 隔离夹具(smoke_all.gd helper 的宿主自持私有副本——源文件收尾整体删除,宿主自给自足)----
# WCAG 相对亮度/对比度(滑轨两段对比度断言用)
func _theme_rel_luminance(c: Color) -> float:
var lin := func(ch: float) -> float:
return ch / 12.92 if ch <= 0.03928 else pow((ch + 0.055) / 1.055, 2.4)
return 0.2126 * lin.call(c.r) + 0.7152 * lin.call(c.g) + 0.0722 * lin.call(c.b)
func _theme_contrast(a: Color, b: Color) -> float:
var la := _theme_rel_luminance(a)
var lb := _theme_rel_luminance(b)
return (maxf(la, lb) + 0.05) / (minf(la, lb) + 0.05)
# 定域流桩(scope 派生种子——derive_at 纯函数,与取号计数器状态无关)
func _mk_stream(stream_name: String, scope: int) -> RandomNumberGenerator:
# stream_name = 调用点自述标签(不参与派生),保留参数零改调用方
var rg := RandomNumberGenerator.new()
rg.seed = _rng.derive_at(scope)
return rg
# 排雷 core 态摘要(同板同序列对照面——已开记录含 kind/digit、封印集、英雄位、AP、相位)
func _ms_digest(ms, ae) -> Array:
var b: Variant = ms.current_board()
var rev: Array = []
for c in ms.revealed_cells():
var rec: Dictionary = ms.reveal_record(c)
rev.append([int(c.x), int(c.y), String(rec.get("kind", "")), int(rec.get("digit", -1))])
rev.sort()
var seals: Array = []
for y in b.height:
for x in b.width:
var c := Vector2i(x, y)
if ms.has_seal(c):
seals.append([int(c.x), int(c.y)])
return [[int(ms.hero_pos().x), int(ms.hero_pos().y)], rev, seals, ae.ap, ae.phase, ms.seal_count()]
# 微光夹具身份解析桩:位点覆盖表 _glow_type_overrides(Vector2i → cell_type)+封锁集
# _glow_blocked(Vector2i → true,返回 null = 不在摇号清单的封锁格)。
# 哈希兜底按坐标确定性散列——全缸可复现。
const _GLOW_FIXTURE_TYPES := ["attack", "defense", "blast", "heal", "weaken",
"pierce", "magic", "action", "gp"]
var _glow_type_overrides := {}
var _glow_blocked := {}
func _glow_fixture_identity(c: Vector2i) -> Variant:
if _glow_blocked.has(c):
return null
var ctype := String(_glow_type_overrides.get(c, ""))
if ctype == "":
ctype = _GLOW_FIXTURE_TYPES[int(abs(c.x * 31 + c.y * 17)) % _GLOW_FIXTURE_TYPES.size()]
return {"reward_id": "fix_%d_%d" % [c.x, c.y], "cell_type": ctype, "tier": 1}
# 英雄 8 邻中未开且未移除的格数(夹具起板前提——初始展开常把 8 邻全揭示)
func _glow_unrevealed_neighbors(ms) -> int:
var n := 0
var hero: Vector2i = ms.hero_pos()
for c in ms.current_board().neighbors8(hero):
if not ms.is_revealed(c) and not ms.is_removed(c):
n += 1
return n
# 独立重算的期望作用集(不读 BoardView 私有域——恒等断言的两侧各自算)
func _glow_expected_set(b, ms) -> Dictionary:
var out := {}
var hero: Vector2i = ms.hero_pos()
for n in b.neighbors8(hero):
if not ms.is_revealed(n) and not ms.is_removed(n):
out[n] = true
return out
# 键集相等(期望集是纯键集——V1 恒等面对照)
func _glow_key_set_equal(a: Dictionary, b: Dictionary) -> bool:
if a.size() != b.size():
return false
for k in a:
if not b.has(k):
return false
return true
# 键值逐位相等(off→on 复现——两侧都是作用集快照,组号亦须逐位一致)
func _glow_map_equal(a: Dictionary, b: Dictionary) -> bool:
if a.size() != b.size():
return false
for k in a:
if not b.has(k) or int(a[k]) != int(b[k]):
return false
return true
# 元进度隔离档夹具(_status_panel_projection/_career_overlay_projection 消费——G9 同名 helper
# 的宿主侧私有副本,双份过渡,收尾批去重)
const _META_SMOKE_PATHS := ["user://smoke_meta_buy.json", "user://smoke_meta_dry.json",
"user://smoke_meta_settle.json", "user://smoke_meta_workshop.json",
"user://smoke_meta_cap.json", "user://smoke_meta_white.json",
"user://smoke_meta_attune.json", "user://smoke_meta_attune2.json",
"user://smoke_meta_tree.json", "user://smoke_meta_career.json",
"user://smoke_meta_career_v5.json", "user://smoke_meta_treasure_unlock.json", "user://smoke_meta_treasure_blank.json",
"user://smoke_meta_forge.json", "user://smoke_meta_forge2.json", "user://smoke_meta_forge3.json",
"user://smoke_meta_forge4.json", "user://smoke_meta_forge5.json"]
func _meta_smoke_cleanup() -> void:
for p in _META_SMOKE_PATHS:
for suffix in ["", ".tmp", ".corrupt.bak"]:
if FileAccess.file_exists(p + suffix):
DirAccess.remove_absolute(p + suffix)
func _meta_new_save(path: String):
var sv = load("res://core/save_manager.gd").new(path)
sv.load_from_disk()
return sv
# ---- 未开格类型微光(作用集恒等/九型归组封闭/零玩法写入)----
func _smoke_type_glow() -> void:
print("--- 未开格类型微光(作用集恒等/九型归组封闭/零玩法写入)---")
var board_generator = load("res://core/board_generator.gd").new()
var b = board_generator.generate(1, 0, _mk_stream("board_gen", 4242), _db)
var ae = load("res://core/action_economy.gd").new(_db)
var ms = load("res://core/minesweeper_rules.gd").new(_db, ae)
ms.install_board(b)
ae.notify_round_start()
# 新手策略驱动到「英雄邻域有未开格」态(初始展开常把 8 邻全揭示——取证探针同款起手法;
# 恒定种子确定性,步数上限 40 防冻态空转)
var steps := 0
while _glow_unrevealed_neighbors(ms) < 5 and steps < 40:
var act: Dictionary = AutoPlayer.novice_action(ms)
if String(act.get("kind", "stop")) == "stop":
break
var op := String(act.get("kind", ""))
if op == "seal":
op = "seal_place"
ms.settle_batch([{"action": op, "cell": act.get("cell")}])
steps += 1
var view = load("res://presentation/views/board_view.gd").new()
view.set_identity_resolver(_glow_fixture_identity)
view.rebuild(b, ms)
view.set_dig_glow_enabled(true)
var hero: Vector2i = ms.hero_pos()
# 快照 trio:棋盘态(零玩法写入的对照面)+作用集
var revealed_before: Array = ms.revealed_cells()
revealed_before.sort()
var hero_before: Vector2i = ms.hero_pos()
var seals_before: int = ms.seal_count()
# ---- 相 0:夹具全集合法九型 ⇒ 作用集 == 独立重算期望集(V1)----
var snap: Dictionary = view.type_glow_probe()
Kit.check("V1 微光默认开(Q4 丙案——呈现态布尔缺省 true,生产写入面零显式开)",
bool(snap.get("enabled", false)))
Kit.check("V1 夹具全集合法九型 ⇒ 清单外登记为空(禁静默空光通道零误报)",
(snap.get("errors", []) as Array).is_empty())
var expect: Dictionary = _glow_expected_set(b, ms)
Kit.check("V1 作用集恒等:微光格集合 == _diggable ∩ 身份可查(期望 %d,实测 %d)" % [expect.size(), (snap.get("cells", {}) as Dictionary).size()],
_glow_key_set_equal(snap.get("cells", {}), expect))
Kit.check("V1 夹具前提:英雄未开邻格 ≥5(实测 %d——novice %d 步)" % [expect.size(), steps],
expect.size() >= 5)
# 已开格/r0 出生展开区零微光(S1:已开格不显示)
var opened_leak := ""
for c in ms.revealed_cells():
if (snap.get("cells", {}) as Dictionary).has(c):
opened_leak += str(c) + " "
for c in b.compute_r0():
if (snap.get("cells", {}) as Dictionary).has(c):
opened_leak += "r0:" + str(c) + " "
Kit.check("V1 已开格/r0 出生展开区零微光(S1)%s" % ("" if opened_leak == "" else "违例:" + opened_leak),
opened_leak == "")
# ---- 相 1:八邻派定八型 ⇒ 五组逐格映射(V2 活板面)----
var nbs: Array = b.neighbors8(hero)
var assign := ["attack", "heal", "pierce", "blast", "magic", "defense", "action", "gp"]
var expect_groups := {"attack": UiKit.GLOW_BUFF, "defense": UiKit.GLOW_BUFF, "weaken": UiKit.GLOW_BUFF,
"action": UiKit.GLOW_BUFF, "gp": UiKit.GLOW_BUFF, "heal": UiKit.GLOW_HEAL,
"pierce": UiKit.GLOW_PIERCE, "blast": UiKit.GLOW_AOE, "magic": UiKit.GLOW_MAGIC}
var mapped := 0
for i in mini(assign.size(), nbs.size()):
var nb: Vector2i = nbs[i]
if ms.is_revealed(nb) or ms.is_removed(nb):
continue # 已开邻格不派定——只测未开格
_glow_type_overrides[nb] = assign[i]
mapped += 1
view.refresh()
snap = view.type_glow_probe()
Kit.check("V2 夹具前提:未开邻格派定 ≥5 型(实测 %d)" % mapped, mapped >= 5)
var map_bad := ""
for i in mini(assign.size(), nbs.size()):
var nb2: Vector2i = nbs[i]
if not _glow_type_overrides.has(nb2):
continue
var got: int = int((snap.get("cells", {}) as Dictionary).get(nb2, 0))
if got != int(expect_groups[assign[i]]):
map_bad += "%s:%s→组%d(期望组%d) " % [str(nb2), assign[i], got, int(expect_groups[assign[i]])]
Kit.check("V2 九型→五组逐格映射(attack/defense/action/gp→增益/heal→治疗/pierce→穿刺/blast→群伤/magic→魔法)%s"
% ("" if map_bad == "" else "违例:" + map_bad),
map_bad == "")
# ---- 相 2:清单外 cell_type ⇒ 显式登记 + 该格无微光(V2 禁静默空光)----
var bogus_cell: Vector2i = hero
for i in mini(assign.size(), nbs.size()):
var nb3: Vector2i = nbs[i]
if _glow_type_overrides.has(nb3):
bogus_cell = nb3
break
_glow_type_overrides[bogus_cell] = "keystone" # 大宝箱未来第十型之外的清单外值
view.refresh()
snap = view.type_glow_probe()
var errs: Array = snap.get("errors", [])
Kit.check("V2 清单外 cell_type 显式登记(errors=%s)" % str(errs), errs.has("keystone"))
Kit.check("V2 清单外格零微光(无静默空光——%s 不在作用集)" % str(bogus_cell),
not (snap.get("cells", {}) as Dictionary).has(bogus_cell))
Kit.check("V2 清单外格剔除后作用集 = 派定数 −1(%d→%d)" % [mapped, (snap.get("cells", {}) as Dictionary).size()],
(snap.get("cells", {}) as Dictionary).size() == mapped - 1)
# ---- 相 3:封锁格(query 返回 null)⇒ 无微光且零登记 ----
_glow_type_overrides.erase(bogus_cell) # 复位清单外(errors 保留=整图登记语义)
var blocked_cell: Vector2i = hero
for i in mini(assign.size(), nbs.size()):
var nb4: Vector2i = nbs[i]
if _glow_type_overrides.has(nb4):
blocked_cell = nb4
break
_glow_blocked[blocked_cell] = true
view.refresh()
snap = view.type_glow_probe()
Kit.check("V1 封锁格零微光(query null ⇒ 天然惰性——%s)" % str(blocked_cell),
not (snap.get("cells", {}) as Dictionary).has(blocked_cell))
Kit.check("V1 封锁格零登记(errors 仍仅清单外一条——%s)" % str(snap.get("errors", [])),
(snap.get("errors", []) as Array).size() == 1)
Kit.check("V1 封锁格剔除后作用集 = 派定数 −1(实测 %d)" % (snap.get("cells", {}) as Dictionary).size(),
(snap.get("cells", {}) as Dictionary).size() == mapped - 1)
# ---- 相 4:封锁解除 ⇒ 作用集复位(进出圈复亮走回)----
_glow_blocked.erase(blocked_cell)
view.refresh()
snap = view.type_glow_probe()
Kit.check("V1 封锁解除后作用集复位恒等(期望 %d,实测 %d)" % [expect.size(), (snap.get("cells", {}) as Dictionary).size()],
_glow_key_set_equal(snap.get("cells", {}), expect))
# ---- 相 5:开关 off/on(V7——off 态空+on 态逐位复现+棋盘态零扰动)----
var snap_before_off: Dictionary = view.type_glow_probe()
view.set_type_glow_enabled(false)
snap = view.type_glow_probe()
Kit.check("V7 off 态开关读数=false", not bool(snap.get("enabled", true)))
Kit.check("V7 off 态作用集为空(基线保护面)", (snap.get("cells", {}) as Dictionary).is_empty())
view.set_type_glow_enabled(true)
var snap_on: Dictionary = view.type_glow_probe()
Kit.check("V7 on 态开关读数=true(复现)", bool(snap_on.get("enabled", false)))
Kit.check("V7 off→on 作用集逐位复现(关前 %d 格/开后 %d 格,组号逐位一致)" % [
(snap_before_off.get("cells", {}) as Dictionary).size(), (snap_on.get("cells", {}) as Dictionary).size()],
_glow_map_equal(snap_on.get("cells", {}), snap_before_off.get("cells", {})))
Kit.check("V7 off→on 作用集与独立重算期望集恒等(%d 格)" % (snap_on.get("cells", {}) as Dictionary).size(),
_glow_key_set_equal(snap_on.get("cells", {}), expect))
var revealed_after: Array = ms.revealed_cells()
revealed_after.sort()
Kit.check("V7 微光开关零玩法写入(已开集/英雄位/封印数三面对照逐位一致)",
revealed_before == revealed_after and hero_before == ms.hero_pos() and seals_before == ms.seal_count())
# ---- 相 6:换图随 rebuild 重算(V3——旧板坐标零残留)----
_glow_type_overrides.clear()
_glow_blocked.clear()
var b2 = board_generator.generate(1, 0, _mk_stream("board_gen", 909), _db)
var ae2 = load("res://core/action_economy.gd").new(_db)
var ms2 = load("res://core/minesweeper_rules.gd").new(_db, ae2)
ms2.install_board(b2)
ae2.notify_round_start()
view.rebuild(b2, ms2)
var snap2: Dictionary = view.type_glow_probe()
var expect2: Dictionary = _glow_expected_set(b2, ms2)
Kit.check("V3 换图 rebuild 后作用集按新板重算(期望 %d,实测 %d——零旧板残留)" % [expect2.size(), (snap2.get("cells", {}) as Dictionary).size()],
_glow_key_set_equal(snap2.get("cells", {}), expect2))
Kit.check("V3 换图清清单外登记(rebuild 清零)", (snap2.get("errors", []) as Array).is_empty())
view.free()
# ---- 相 7:同板同序列 on/off 双实例结算逐位一致(契约 1 端到端面——不新起跑批)----
# 两个平行 core(同 db/同一 BoardData 产物/同初始展开),一侧挂生产默认开态 view、一侧挂
# 显式 off 态 view(差异唯一定位在类型微光开关——与渲染取证双帧同款隔离思想);同一动作
# 序列逐步对照摘要,on 侧若在微光派生中意外写板态,两侧即分叉。
var aeA = load("res://core/action_economy.gd").new(_db)
var msA = load("res://core/minesweeper_rules.gd").new(_db, aeA)
var aeB = load("res://core/action_economy.gd").new(_db)
var msB = load("res://core/minesweeper_rules.gd").new(_db, aeB)
msA.install_board(b)
msB.install_board(b)
aeA.notify_round_start()
aeB.notify_round_start()
var viewA = load("res://presentation/views/board_view.gd").new()
viewA.set_identity_resolver(_glow_fixture_identity)
viewA.rebuild(b, msA)
viewA.set_dig_glow_enabled(true)
var viewB = load("res://presentation/views/board_view.gd").new()
viewB.set_identity_resolver(_glow_fixture_identity)
viewB.rebuild(b, msB)
viewB.set_dig_glow_enabled(true)
viewB.set_type_glow_enabled(false) # off 态 = 基线保护面
var diverge := ""
var seq_steps := 0
while seq_steps < 12:
var act7: Dictionary = AutoPlayer.novice_action(msA)
if String(act7.get("kind", "stop")) == "stop":
break
var op7 := String(act7.get("kind", ""))
if op7 == "seal":
op7 = "seal_place"
var batch7 := [{"action": op7, "cell": act7.get("cell")}]
msA.settle_batch(batch7)
msB.settle_batch(batch7)
viewA.refresh()
viewB.refresh()
if _ms_digest(msA, aeA) != _ms_digest(msB, aeB):
diverge = "第 %d 步(%s)后摘要分叉" % [seq_steps, op7]
break
seq_steps += 1
Kit.check("契约 1 同板同序列 on/off 两态结算逐位一致(%d 步对照——A 开态/B 关态)%s"
% [seq_steps, diverge],
diverge == "")
Kit.check("契约 1 对照前提:同序列 ≥5 步(实测 %d)" % seq_steps, seq_steps >= 5)
viewA.free()
viewB.free()
# ---- 战斗日志视图·行格式化呈现层同步闸(真人试玩卡断 bug 回归:
# 伤害行 attach 字段变更后本视图拆行分支漏网未同步——敌方首笔攻击行带 baseline 无 attach,
# _format_row Invalid access 使结算段中途夭折、界面卡死。行协议变更必须同步呈现层〔战斗结算 §行协议〕,
# 本段把现行行形态逐型喂进真实视图:拆行残留与行字段错位在此转红)----
func _smoke_battle_log_view() -> void:
print("--- 战斗日志视图行格式化(行协议 9 类呈现层同步闸)---")
var view = load("res://presentation/views/battle_log_view.gd").new(_db)
# 敌方行动伤害行(现行形态:baseline 在场、attach 已出协议——修复前此行即 Invalid access)
view.append_row({"type": "damage", "display_name": "地精 撕咬", "value": 2, "target": "主角",
"baseline": 3, "roll": 15, "ac": 12, "tier": 100,
"source_class": "enemy_action", "round_index": 2})
var enemy_txt: String = view.parsed_text()
Kit.check("敌方伤害行格式化(baseline 行不读退役 attach)",
enemy_txt.contains("撕咬") and enemy_txt.contains("〔掷 15 vs 护甲 12"))
Kit.check("退役伏击/基线拆行文案不再出现",
not enemy_txt.contains("伏击") and not enemy_txt.contains("基线"))
# 主角攻击行·单分量模型(payload 只带实际攻击类型一键——pierce/magic 多键即崩)
view.append_row({"type": "damage", "display_name": "主角攻击", "value": 4, "pierce": 4,
"roll": 20, "ac": 13, "tier": 100, "crit": true, "killed": true,
"source_class": "hero_attack", "round_index": 2})
var hero_txt: String = view.parsed_text()
Kit.check("主角穿刺型暴击行金框(单分量 payload 不触发三分量拆行)",
hero_txt.contains("暴击×") and hero_txt.contains("⚔击杀") and hero_txt.contains("■"))
Kit.check("退役三分量拆行文案不再出现", not hero_txt.contains(" + 穿刺 "))
# 未生效行聚合(pattern 3 聚合纪律——同键两行呈现 ×2 不逐行刷屏)
view.append_row({"type": "reward_ineffective", "display_name": "增益未生效(满层)", "batch_key": "attack_up", "source_class": "safe_cell", "round_index": 1})
view.append_row({"type": "reward_ineffective", "display_name": "增益未生效(满层)", "batch_key": "attack_up", "source_class": "safe_cell", "round_index": 1})
view.append_row({"type": "heal", "display_name": "急救包", "value": 3, "target": "主角", "source_class": "safe_cell", "round_index": 1})
var agg_txt: String = view.parsed_text()
Kit.check("未生效行聚合计数 ×2 呈现", agg_txt.contains("⚠") and agg_txt.contains("×2"))
# 其余行型逐型喂入:格式化中途夭折 = 解析文本零增长(脚本错误静默吞断言的盲区由此转为可见 FAIL)
var rest := {
"effect": {"type": "effect", "display_name": "攻击强化", "value": 1, "pool_value": 2, "effect_id": "attack_up", "source_class": "seal_hit", "round_index": 1},
"seal_miss": {"type": "seal_miss", "display_name": "封印未中", "source_class": "seal_miss", "round_index": 1},
"cast": {"type": "cast", "display_name": "卓尔精灵 施放 毒雾", "source_class": "enemy_action", "round_index": 3},
"decay": {"type": "decay", "value": 1, "round_index": 3},
"verdict": {"type": "verdict", "display_name": "下一轮次", "value": 0, "round_index": 3},
"highlight": {"type": "highlight", "display_name": "完美封印!清图奖励 ×2", "value": 2, "source_class": "clear_reward", "round_index": 4},
}
for row_type in rest:
var before: int = view.parsed_text().length()
view.append_row(rest[row_type])
Kit.check("行型 %s 格式化落地(解析文本增长)" % String(row_type),
view.parsed_text().length() > before)
# ---- UI 全局主题冒烟(需求卡片 §7 验收清单 V1-V6)----
# V1 挂载覆盖 / V2 滑轨两段 / V3 预盖件运行时证词 / V4 旧形态零残留 /
# V5 回归(本冒烟全绿即回归网)/ V6 中文字体防御(挂主题后 default_font 顶掉 custom_font 也不丢字形)
func _smoke_global_theme() -> void:
print("--- UI 全局主题(main_theme.tres 挂载与词条断言)---")
var th := ThemeDB.get_project_theme()
Kit.check("V1 项目主题挂载(project.godot gui/theme/custom → main_theme.tres)", th != null)
if th == null:
return
# V1:类型集覆盖 §4「做」清单全部类型(含 PopupMenu 弹层预盖)
var need_types := ["VScrollBar", "HScrollBar", "ScrollContainer", "HSlider", "VSlider",
"TooltipPanel", "TooltipLabel", "LineEdit", "SpinBox", "HSeparator", "VSeparator",
"CheckBox", "CheckButton", "OptionButton", "PopupMenu"]
var type_set := th.get_type_list()
for t in need_types:
Kit.check("V1 主题类型覆盖 %s" % t, type_set.has(t))
# V1 红线:Button 类型词条零接触(五态矩阵归 06 第一层第二条)
Kit.check("V1 红线 Button 类型词条零接触", not type_set.has("Button"))
# V1 红线:focus 族词条一律零定义(扫本主题自有条目,不走默认主题链)
var focus_hits := ""
for t in type_set:
for n in (th.get_stylebox_list(t) + th.get_icon_list(t) + th.get_color_list(t)
+ th.get_font_list(t) + th.get_font_size_list(t) + th.get_constant_list(t)):
if "focus" in String(n):
focus_hits += "%s/%s " % [t, n]
Kit.check("V1 红线 focus 族词条零定义%s" % ("" if focus_hits == "" else "(违例:" + focus_hits + ")"),
focus_hits == "")
# V1:关键词条抽查在位(实例走主题链解析)
var vsb := VScrollBar.new()
Kit.check("V1 滚动条槽/柄三态词条在位",
vsb.has_theme_stylebox("scroll") and vsb.has_theme_stylebox("grabber")
and vsb.has_theme_stylebox("grabber_highlight") and vsb.has_theme_stylebox("grabber_pressed"))
Kit.check("V1 滚动条 scroll_focus 未定义(红线)",
not th.has_stylebox("scroll_focus", "VScrollBar") and not th.has_stylebox("scroll_focus", "HScrollBar"))
var hsl := HSlider.new()
Kit.check("V1 滑轨槽/填充/柄词条在位",
hsl.has_theme_stylebox("slider") and hsl.has_theme_stylebox("grabber_area")
and hsl.has_theme_stylebox("grabber_area_highlight")
and hsl.has_theme_icon("grabber") and hsl.has_theme_icon("grabber_highlight")
and hsl.has_theme_icon("grabber_disabled"))
var le := LineEdit.new()
Kit.check("V1 LineEdit 词条在位(含占位色 font_placeholder_color)",
le.has_theme_stylebox("normal") and le.has_theme_color("font_color")
and le.has_theme_color("font_placeholder_color"))
var sp := SpinBox.new()
Kit.check("V1 SpinBox 箭头词条在位(updown 合并槽)", sp.has_theme_icon("updown"))
var hsep := HSeparator.new()
var vsep := VSeparator.new()
Kit.check("V1 分隔线词条在位",
hsep.has_theme_stylebox("separator") and vsep.has_theme_stylebox("separator"))
var tpanel := PanelContainer.new()
var tlabel := Label.new()
Kit.check("V1 TooltipPanel 词条在位", tpanel.has_theme_stylebox("panel"))
Kit.check("V1 TooltipLabel 词条在位",
tlabel.has_theme_color("font_color") and tlabel.has_theme_font_size("font_size"))
# V2:滑轨修复——已填充段 BG_ROOT 对槽暖石 ≈5.5:1(冻结值锚定)
var fill_sb: StyleBox = hsl.get_theme_stylebox("grabber_area")
var track_sb: StyleBox = hsl.get_theme_stylebox("slider")
if fill_sb is StyleBoxFlat and track_sb is StyleBoxFlat:
var fill_c: Color = (fill_sb as StyleBoxFlat).bg_color
var track_c: Color = (track_sb as StyleBoxFlat).bg_color
Kit.check("V2 冻结值锚定(已填充段=BG_ROOT #3A2E22,槽=暖石 #B8A582)",
fill_c.is_equal_approx(Color8(58, 46, 34)) and track_c.is_equal_approx(Color8(184, 165, 130)))
var ratio := _theme_contrast(fill_c, track_c)
Kit.check("V2 滑轨两段对比 %.2f:1 ≥4.5:1(拖到中点一眼可辨)" % ratio, ratio >= 4.5)
else:
Kit.check("V2 滑轨 stylebox 类型均为 StyleBoxFlat", false)
# V3:预盖件运行时证词(临时实例化读词条——无实役控件的在役证据)
var cbx := CheckBox.new()
Kit.check("V3 CheckBox 四态图标非空",
cbx.get_theme_icon("checked") != null and cbx.get_theme_icon("unchecked") != null
and cbx.get_theme_icon("checked_disabled") != null and cbx.get_theme_icon("unchecked_disabled") != null)
var cbt := CheckButton.new()
Kit.check("V3 CheckButton 四态图标非空(与 CheckBox 同套 checked/unchecked 名,4.7 源码实证)",
cbt.get_theme_icon("checked") != null and cbt.get_theme_icon("unchecked") != null
and cbt.get_theme_icon("checked_disabled") != null and cbt.get_theme_icon("unchecked_disabled") != null)
var pm := PopupMenu.new()
Kit.check("V3 PopupMenu 词条存在(弹层预盖)",
pm.has_theme_stylebox("panel") and pm.has_theme_stylebox("hover")
and pm.has_theme_font_size("font_size"))
var ob := OptionButton.new()
Kit.check("V3 OptionButton 词条存在(预盖)",
ob.has_theme_stylebox("normal") and ob.has_theme_stylebox("hover")
and ob.has_theme_stylebox("pressed") and ob.has_theme_color("font_color"))
# V4:旧形态源码零残留(剥注释行后扫;ui_kit 两函数+挂载行+滑轨工厂)
for path in ["res://presentation/views/ui_kit.gd", "res://presentation/ui_assembler.gd",
"res://presentation/views/settings_overlay.gd", "res://presentation/views/debug_overlay.gd"]:
var src: String = FileAccess.get_file_as_string(path)
var bad := ""
for ln in src.split("\n", false):
var s := ln.strip_edges()
if s.begins_with("#"):
continue
if "styled_slider" in s or "tooltip_theme" in s or "_tip_theme" in s or "overlay.theme" in s:
bad += s + " "
Kit.check("V4 退役实现零残留(%s)%s" % [path, "" if bad == "" else "违例:" + bad],
bad == "")
# V6:中文字体防御——default_font 非空且 Label 实取字体非空
Kit.check("V6 主题 default_font 非空(挂主题顶掉 custom_font 也不丢中文字形)", th.default_font != null)
var lbl := Label.new()
lbl.text = "测试"
Kit.check("V6 Label 实取字体非空(中文字形防御实证)", lbl.get_theme_font("font") != null)
vsb.free(); hsl.free(); le.free(); sp.free(); hsep.free(); vsep.free()
tpanel.free(); tlabel.free(); cbx.free(); cbt.free(); pm.free(); ob.free(); lbl.free()
# ---- 按钮五态矩阵冒烟(四工厂产物逐实例 override 断言;V1/V2)----
func _smoke_button_five_states() -> void:
print("--- UI 按钮五态矩阵(四工厂五态样式盒)---")
var uikit_s = load("res://presentation/views/ui_kit.gd")
var states := ["normal", "hover", "pressed", "disabled", "focus"]
# 普通钮(verb_button 同走 button() 打底——同断言覆盖)
var b = uikit_s.button("测试")
for st in states:
Kit.check("V1 普通钮 %s 样式盒非空" % st, b.has_theme_stylebox_override(st))
var n_sb: StyleBox = b.get_theme_stylebox("normal")
var h_sb: StyleBox = b.get_theme_stylebox("hover")
var p_sb: StyleBox = b.get_theme_stylebox("pressed")
var d_sb: StyleBox = b.get_theme_stylebox("disabled")
var f_sb: StyleBox = b.get_theme_stylebox("focus")
Kit.check("V1 普通钮 normal/hover/disabled 均为石纹九宫格贴图样式盒",
n_sb is StyleBoxTexture and h_sb is StyleBoxTexture and d_sb is StyleBoxTexture)
# 悬停=乙案:modulate 提亮(≠常态 WHITE——乙案判据)
var h_mod: Color = (h_sb as StyleBoxTexture).modulate_color
Kit.check("V1 普通钮 hover=modulate 提亮(乙案,异于常态 WHITE)",
h_mod != Color.WHITE and h_mod.r > 1.0)
# 禁用=甲案:本族底+α0.45 幽灵化
var d_mod: Color = (d_sb as StyleBoxTexture).modulate_color
Kit.check("V1 普通钮 disabled=本族底 α0.45 幽灵化(甲案)",
is_equal_approx(d_mod.a, 0.45) and d_mod.r == 1.0 and d_mod.g == 1.0)
# 错配修正判据:disabled≠pressed(旧「禁用=按压图」)
Kit.check("V1 普通钮 disabled≠pressed(旧错配已修正)", _sb_content_differs(d_sb, p_sb))
# 焦点=甲案:INK 2px 内缩 1px 环、无底(叠加态不遮钮底)
Kit.check("V1 普通钮 focus 样式盒为扁平环(StyleBoxFlat)", f_sb is StyleBoxFlat)
var fsb := f_sb as StyleBoxFlat
Kit.check("V1 焦点环色=INK(与 font_focus_color 同源)", fsb.border_color == uikit_s.INK)
Kit.check("V1 焦点环宽=2px",
is_equal_approx(fsb.border_width_left, 2.0) and is_equal_approx(fsb.border_width_right, 2.0)
and is_equal_approx(fsb.border_width_top, 2.0) and is_equal_approx(fsb.border_width_bottom, 2.0))
Kit.check("V1 焦点环内缩=1px",
is_equal_approx(fsb.expand_margin_left, -1.0) and is_equal_approx(fsb.expand_margin_top, -1.0)
and is_equal_approx(fsb.expand_margin_right, -1.0) and is_equal_approx(fsb.expand_margin_bottom, -1.0))
Kit.check("V1 焦点环无底(叠加态不遮钮底)", not fsb.draw_center and fsb.bg_color.a < 0.001)
b.free()
# 主钮:五态非空+本族平涂(pressed/disabled 非石纹)+disabled≠pressed+hover 提亮档承现行
var pb = uikit_s.primary_button("测试")
for st in states:
Kit.check("V1 主钮 %s 样式盒非空" % st, pb.has_theme_stylebox_override(st))
var ppn: StyleBox = pb.get_theme_stylebox("pressed")
var pdd: StyleBox = pb.get_theme_stylebox("disabled")
var phv: StyleBox = pb.get_theme_stylebox("hover")
Kit.check("V1 主钮 pressed/disabled=本族平涂(非石纹)",
ppn is StyleBoxFlat and pdd is StyleBoxFlat)
Kit.check("V1 主钮 pressed=烛光黄深档(现行值不动)",
(ppn as StyleBoxFlat).bg_color == Color8(214, 172, 88))
Kit.check("V1 主钮 disabled=本族底 α0.45 幽灵化",
is_equal_approx((pdd as StyleBoxFlat).bg_color.a, 0.45))
Kit.check("V1 主钮 disabled≠pressed", _sb_content_differs(ppn, pdd))
Kit.check("V1 主钮 hover=提亮档(承现行不动)",
(phv as StyleBoxFlat).bg_color == uikit_s.CANDLE.lightened(0.06))
pb.free()
# 危险钮:补本族 pressed(深档)+disabled 幽灵化+focus;pressed/disabled 非石纹
var db = uikit_s.danger_button("测试")
for st in states:
Kit.check("V1 危险钮 %s 样式盒非空" % st, db.has_theme_stylebox_override(st))
var dpr: StyleBox = db.get_theme_stylebox("pressed")
var dds: StyleBox = db.get_theme_stylebox("disabled")
Kit.check("V1 危险钮 pressed/disabled=本族平涂(非石纹,旧错配已修正)",
dpr is StyleBoxFlat and dds is StyleBoxFlat)
Kit.check("V1 危险钮 pressed=深红深档(暗于常态)",
(dpr as StyleBoxFlat).bg_color.r < 180.0 / 255.0)
Kit.check("V1 危险钮 disabled=本族底 α0.45 幽灵化",
is_equal_approx((dds as StyleBoxFlat).bg_color.a, 0.45))
Kit.check("V1 危险钮 disabled≠pressed", _sb_content_differs(dpr, dds))
db.free()
# V2 主题红线维持(Button 类型词条零接触+focus 族词条零定义)——与 _smoke_global_theme 同判据复述
var th := ThemeDB.get_project_theme()
if th != null:
Kit.check("V2 红线维持 Button 类型词条零接触", not th.get_type_list().has("Button"))
var focus_hits := ""
for t in th.get_type_list():
for nm in (th.get_stylebox_list(t) + th.get_icon_list(t) + th.get_color_list(t)
+ th.get_font_list(t) + th.get_font_size_list(t) + th.get_constant_list(t)):
if "focus" in String(nm):
focus_hits += "%s/%s " % [t, nm]
Kit.check("V2 红线维持 focus 族词条零定义%s" % ("" if focus_hits == "" else "(违例:" + focus_hits + ")"),
focus_hits == "")
# ---- 全局纹理过滤冒烟(工程设置 Nearest+平滑渐变件 LINEAR 白名单;V3)----
func _smoke_texture_filter() -> void:
print("--- UI 全局纹理过滤(Nearest+白名单三件)---")
var v = ProjectSettings.get_setting("rendering/textures/canvas_textures/default_texture_filter")
Kit.check("V3 工程设置 default_texture_filter=0(Nearest,翻转面=立绘/瓦片/九宫格 crisp)",
v != null and int(v) == 0)
var uikit_s = load("res://presentation/views/ui_kit.gd")
var vg = uikit_s.vignette_rect()
Kit.check("V3 白名单在位:暗角 vignette 显式 LINEAR",
vg.texture_filter == CanvasItem.TEXTURE_FILTER_LINEAR)
vg.free()
var bm = uikit_s.beams_rect()
Kit.check("V3 白名单在位:光柱 beams 显式 LINEAR",
bm.texture_filter == CanvasItem.TEXTURE_FILTER_LINEAR)
# T4 光柱连续 alpha 结构断言(观感归目检):带心峰值 0.05/带缘连续过渡/纵向衰减上亮下暗
var bimg: Image = bm.texture.get_image()
var max_a := 0.0
var mid_a := 0
var top_sum := 0.0
var bot_sum := 0.0
for y in 560:
for x in 560:
var pa := bimg.get_pixel(x, y).a
max_a = maxf(max_a, pa)
if pa > 0.001 and pa < 0.049:
mid_a += 1
if y < 280:
top_sum += pa
else:
bot_sum += pa
Kit.check("T4 光柱带心峰值 α≈0.05(8bit 截断量化下限 12/255,实测 %.5f)" % max_a,
max_a >= 12.0 / 255.0 and max_a <= 0.05)
Kit.check("T4 光柱带内连续 alpha 过渡像素 %d 个(旧零过渡硬边已修)" % mid_a, mid_a > 100)
Kit.check("T4 光柱纵向衰减生效(上半亮度 %.0f > 下半 %.0f 的 2 倍——上亮下暗)" % [top_sum, bot_sum],
top_sum > bot_sum * 2.0)
bm.free()
var gb = uikit_s.gradient_base(Color.BLACK, Color.WHITE)
Kit.check("V3 白名单在位:地图渐变 gradient_base 显式 LINEAR",
gb.texture_filter == CanvasItem.TEXTURE_FILTER_LINEAR)
gb.free()
# 既有显式 LINEAR 两处不丢(源码锚定——脚影/状态图标)
var bw_src: String = FileAccess.get_file_as_string("res://presentation/views/board_world.gd")
Kit.check("V3 脚影显式 LINEAR 不丢(board_world.gd)",
bw_src.contains("shadow.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR"))
var ss_src: String = FileAccess.get_file_as_string("res://presentation/views/status_slot_row.gd")
Kit.check("V3 状态图标显式 LINEAR 不丢(status_slot_row.gd)",
ss_src.contains("icon.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR"))
# ---- <4.5:1 文字位换墨冒烟(站桩徽标两族亮档+RAP 行深石墨;V4)----
func _smoke_contrast_inks() -> void:
print("--- UI 残余 <4.5:1 文字位换墨(V4 对比度锚定)---")
var uikit_s = load("res://presentation/views/ui_kit.gd")
var stage: Color = uikit_s.STAGE_BG
var r_order := _theme_contrast(uikit_s.GOLD_BRIGHT, stage)
Kit.check("V4 顺位徽标/重甲徽标 GOLD_BRIGHT 对舞台底 %.2f:1 ≥4.5:1" % r_order, r_order >= 4.5)
var r_ward := _theme_contrast(uikit_s.HERO_BLUE_BRIGHT, stage)
Kit.check("V4 魔法护甲徽标 HERO_BLUE_BRIGHT 对舞台底 %.2f:1 ≥4.5:1" % r_ward, r_ward >= 4.5)
var r_rap := _theme_contrast(uikit_s.STONE_DEEP, uikit_s.PANEL_DATA)
Kit.check("V4 RAP 反馈行 STONE_DEEP 对数据板 %.2f:1 ≥4.5:1" % r_rap, r_rap >= 4.5)
# 消费点源码锚定:新墨在位、旧裸用点清零、语义岗常量不动
var sb_src: String = FileAccess.get_file_as_string("res://presentation/views/standee_body.gd")
Kit.check("V4 顺位徽标①②③ 墨=GOLD_BRIGHT(13px 行原位)",
sb_src.contains("HORIZONTAL_ALIGNMENT_LEFT, -1, 13, UiKit.GOLD_BRIGHT"))
Kit.check("V4 魔法护甲徽标墨=HERO_BLUE_BRIGHT", sb_src.contains("col = UiKit.HERO_BLUE_BRIGHT"))
Kit.check("V4 重甲徽标墨=GOLD_BRIGHT", sb_src.contains("col = UiKit.GOLD_BRIGHT"))
Kit.check("V4 GOLD_DEEP 语义岗不动(standee:199 意图动画判据)",
sb_src.contains("if _intent_col == UiKit.GOLD_DEEP:"))
var ap_src: String = FileAccess.get_file_as_string("res://presentation/views/ap_bar_view.gd")
Kit.check("V4 RAP 反馈行墨=STONE_DEEP(原位行锚定)",
ap_src.contains('UiKit.label("",UiKit.FS_CAPTION, false, UiKit.STONE_DEEP)'))
Kit.check("V4 RAP 反馈行旧墨 STONE_FAINT 清零(含注记面)", not ap_src.contains("STONE_FAINT"))
# ---- 元素细节余②③冒烟(BCard 图标槽明度差降档+商店居中+营地四栏等高)----
func _smoke_element_details() -> void:
print("--- UI 元素细节余②③(图标槽降档+布局修)---")
var uikit_s = load("res://presentation/views/ui_kit.gd")
# ②白图标 ≥3:1 图形对比硬约束+降档方向锚定
var r_white := _theme_contrast(Color.WHITE, uikit_s.SLOT_BACKDROP_SOFT)
Kit.check("T5② BCard 图标槽白图标对比 %.2f:1 ≥3:1(硬约束)" % r_white, r_white >= 3.0)
var r_new := _theme_contrast(uikit_s.SLOT_BACKDROP_SOFT, uikit_s.PANEL_DATA)
var r_old := _theme_contrast(uikit_s.SLOT_BACKDROP, uikit_s.PANEL_DATA)
Kit.check("T5② 明度差降档生效(槽对卡面 %.2f:1 < 旧 %.2f:1)" % [r_new, r_old], r_new < r_old)
var uk_src: String = FileAccess.get_file_as_string("res://presentation/views/ui_kit.gd")
Kit.check("T5② BCard 图标槽落柔和档(SLOT_BACKDROP_SOFT)",
uk_src.contains("UiKit.panel_flat(SLOT_BACKDROP_SOFT, PANEL_BORDER, 0, 1)"))
var ss_src: String = FileAccess.get_file_as_string("res://presentation/views/status_slot_row.gd")
Kit.check("T5② B 区状态图标槽仍用深档(SLOT_BACKDROP,未误伤)",
ss_src.contains("UiKit.SLOT_BACKDROP"))
# ③布局修源码锚定
var shop_src: String = FileAccess.get_file_as_string("res://presentation/views/shop_overlay_view.gd")
Kit.check("T5③ 商店卡片区垂直居中(ALIGNMENT_CENTER)",
shop_src.contains("_cards_box.alignment = BoxContainer.ALIGNMENT_CENTER"))
var camp_src: String = FileAccess.get_file_as_string("res://presentation/views/camp_overlay_view.gd")
var camp_min := camp_src.count("custom_minimum_size = Vector2(0, 336)")
Kit.check("T5③ 营地三 holder 统一 min 高 336(实测 %d 处)" % camp_min, camp_min == 3)
var camp_fill := camp_src.count("size_flags_vertical = Control.SIZE_FILL")
Kit.check("T5③ 营地四栏面板纵向 FILL 等高(实测 %d 处)" % camp_fill, camp_fill == 4)
Kit.check("T5③ 三处锁文垂直居中在位", camp_src.count("SIZE_SHRINK_CENTER") >= 3)
# ---- 诊断转储钮冒烟(角落钮不在玩家画面+转储入口在调试浮窗;V6)----
func _smoke_diag_button_retired() -> void:
print("--- UI 诊断转储钮退役(V6:玩家画面清零+浮窗可达)---")
var main_src: String = FileAccess.get_file_as_string("res://presentation/main.gd")
Kit.check("V6 主区无 build_diag_button 调用残留(旧实现同批清理)",
not main_src.contains("build_diag_button"))
Kit.check("V6 转储入口连接随迁(组合根接浮窗 diagnose_requested 信号)",
main_src.contains("debug_overlay.diagnose_requested.connect(_on_diagnose_pressed)"))
Kit.check("V6 转储编排承接函数在位(dump_diagnosis 卡死取证保留)",
main_src.contains("func _on_diagnose_pressed()"))
var asm_src: String = FileAccess.get_file_as_string("res://presentation/ui_assembler.gd")
Kit.check("V6 build_diag_button 工厂已退役(ui_assembler 零残留)",
not asm_src.contains("build_diag_button"))
var dbg_src: String = FileAccess.get_file_as_string("res://presentation/views/debug_overlay.gd")
Kit.check("V6 调试浮窗 diagnose_requested 信号在位", dbg_src.contains("signal diagnose_requested"))
Kit.check("V6 浮窗转储钮外发方法在位(纯视图边界)", dbg_src.contains("_emit_diagnose_requested"))
Kit.check("V6 浮窗诊断转储钮在位(玩家画面角落钮清零后唯一入口)",
dbg_src.contains('UiKit.button("转储")'))
# ---- 状态面板只读投影一致性(R4 数据只读同源:状态面板展示值逐项 == MetaProgression
# 访问器读数;投影拼装与 main._status_data 同构(同访问器族同序)。只读性连接巡检与零动效
# 断言归 ui_struct_probe 呈现侧,本段只管数据面)----
func _status_panel_projection() -> void:
print("--- 状态面板·只读投影一致性(隔离档 smoke_meta_buy 复用)---")
_meta_smoke_cleanup()
var sv = _meta_new_save(_META_SMOKE_PATHS[0])
var mp = load("res://core/meta_progression.gd").new(_db, sv)
# 已购态夹具:余额非零 + 秘宝匣首件解锁——投影可辨非全零白板
sv.add_keystone(37)
sv.unlock_item("morning_order")
sv.write_to_disk()
# 投影拼装(main._status_data 同构)+ 视图喂入
var data := {"balance": mp.keystone_balance(), "workshop": mp.treasure_shop_status(),
"tree": mp.tree_camp_rows(), "fork_groups": mp.tree_fork_groups()}
var po = load("res://presentation/views/status_panel.gd").new()
po.open_status(data)
Kit.check("状态面板开层可见(open_status 直开——P4 甲零动效路径)", po.visible)
Kit.check("余额徽投影 = keystone_balance() 读数(%s)" % po._balance_badge.value_label.text,
String(po._balance_badge.value_label.text) == "钥石 %d" % mp.keystone_balance())
var ws: Array = mp.treasure_shop_status()
var ws_ok: bool = po._workshop_holder.get_child_count() == ws.size()
var uikit_s = load("res://presentation/views/ui_kit.gd")
for i in mini(ws.size(), po._workshop_holder.get_child_count()):
var row: Dictionary = ws[i]
var lbls2: Array = []
for ch in po._workshop_holder.get_child(i).get_children():
if ch is Label:
lbls2.append(ch)
var unlocked: bool = bool(row.get("unlocked", false))
var expect_prog := "已入手"
if not unlocked:
if String(row.get("mode", "")) == "keystone":
expect_prog = "钥石 %d" % int(row.get("price", 0))
elif String(row.get("mode", "")) == "condition":
var parts: Array[String] = []
for req_v2 in (row.get("reqs", []) as Array):
var req2: Dictionary = req_v2 as Dictionary
var lbl2 := String(uikit_s.TREASURE_REQ_LABELS.get(String(req2.get("key", "")), "条件"))
parts.append("%s %d/%d" % [lbl2, int(req2.get("progress", 0)), int(req2.get("threshold", 0))])
expect_prog = "、".join(parts)
var expect_st2 := "已解锁" if unlocked else "未解锁"
ws_ok = ws_ok and lbls2.size() == 3 \
and String(lbls2[0].text) == String(row.get("display_name", "")) \
and String(lbls2[1].text) == expect_prog \
and String(lbls2[2].text) == expect_st2
Kit.check("秘宝匣 12 行投影 = treasure_shop_status() 逐项(名称/钥石价与条件进度/解锁态——首件解锁夹具在列)〔秘宝批 V19 呈现数据源〕",
ws_ok)
var owned := 0
for trow in (mp.tree_camp_rows() as Array):
if int(trow.get("level", 0)) > 0:
owned += 1
var tree_txt := String(po._tree_label.text)
Kit.check("天赋树概览聚合文案与已购节点数一致(零已购=空态句)",
tree_txt == "尚未购买任何节点(天赋树在营地主柜台)" or tree_txt.begins_with("已购 %d 项:" % owned))
po.visible = false
po.free()
# ---- 生涯页只读投影一致性(汇总段六类九项逐行 == SaveManager 计数访问器/
# MetaProgression 访问器读数;投影拼装与 main._career_data 同构(同访问器族同序)。
# 只读性连接巡检与零动效断言归 ui_struct_probe 呈现侧,本段只管数据面+旧档载入显示
# (v5 样档升 v6——完成定义 6「计数域缺省补零、页正常显示」))----
func _career_overlay_projection() -> void:
print("--- 生涯页·只读投影一致性(隔离档 smoke_meta_career/career_v5)---")
var sv = _meta_new_save(_META_SMOKE_PATHS[9])
var mp = load("res://core/meta_progression.gd").new(_db, sv)
# 已玩态夹具:两局收束(一通一止)+ 钥石非零——投影可辨非全零白板
# (生涯页不统计天赋树与账号成长态,只统计终身计数)
sv.add_keystone(45)
sv.apply_adventure_summary({"result": "completed", "cleared": true, "levels_cleared": 3,
"stopped_at": 0, "keystone_total": 0, "master_seed": 21})
sv.apply_adventure_summary({"result": "abandoned", "cleared": false, "levels_cleared": 1,
"stopped_at": 2, "keystone_total": 0, "master_seed": 22})
sv.write_to_disk()
# 投影拼装(main._career_data 同构)+ 视图喂入
var data := {"total": sv.adventure_total(), "cleared": sv.adventure_cleared(),
"clears": [], "boss_first_kills": 0, "boss_first_total": _db.level_count(),
"boss_kills": sv.boss_kill_count(), "keystone": mp.keystone_balance()}
for i in _db.level_count():
var li: int = i + 1
data["clears"].append({"display_name": String(_db.level_row(li).get("display_name", "")),
"count": sv.clear_count(li)})
if sv.boss_first_kill_marked(li):
data["boss_first_kills"] += 1
var co = load("res://presentation/views/career_overlay.gd").new()
co.open_career(data)
Kit.check("生涯页开层可见(open_career 直开——零动效路径)", co.visible)
Kit.check("总局数投影 = adventure_total() 读数(%s)" % co._total_label.text,
String(co._total_label.text) == "2 局")
Kit.check("通关局数投影 = adventure_cleared() 读数(%s)" % co._cleared_label.text,
String(co._cleared_label.text) == "1 局(三关全通)")
var expect_clears: Array[String] = []
for row in (data["clears"] as Array):
expect_clears.append("%s ×%d" % [String(row.get("display_name", "")), int(row.get("count", 0))])
Kit.check("各关通关次数投影 = clear_count() 逐关读数(%s)" % co._clears_label.text,
String(co._clears_label.text) == " · ".join(expect_clears))
Kit.check("Boss 首杀/总杀投影 = 首杀标记计数 + boss_kill_count()(%s / %s)" % [co._boss_first_label.text, co._boss_kills_label.text],
String(co._boss_first_label.text) == "3/3 关" and String(co._boss_kills_label.text) == "4 次")
Kit.check("钥石余额徽投影 = keystone_balance() 读数(%s)" % co._keystone_badge.value_label.text,
String(co._keystone_badge.value_label.text) == "钥石 %d" % mp.keystone_balance())
co.visible = false
co.free()
# 旧档载入显示(完成定义 6):v5 现役档形态(计数域两键不存在)升 v6 → 生涯页正常呈现缺省值
var v5 = load("res://core/save_manager.gd").new(_META_SMOKE_PATHS[10])
var fv5c := FileAccess.open(_META_SMOKE_PATHS[10], FileAccess.WRITE)
fv5c.store_string(JSON.stringify({"version": 5, "meta": {"keystone_balance": 3}}))
fv5c.close()
Kit.check("v5 现役档升 v6 载入成功(计数域缺省补零)",
v5.load_from_disk() and v5.adventure_total() == 0 and v5.adventure_cleared() == 0)
var mp_v5 = load("res://core/meta_progression.gd").new(_db, v5)
var data_v5 := {"total": v5.adventure_total(), "cleared": v5.adventure_cleared(),
"clears": [], "boss_first_kills": 0, "boss_first_total": _db.level_count(),
"boss_kills": v5.boss_kill_count(), "keystone": mp_v5.keystone_balance()}
for i in _db.level_count():
data_v5["clears"].append({"display_name": String(_db.level_row(i + 1).get("display_name", "")),
"count": v5.clear_count(i + 1)})
var co_v5 = load("res://presentation/views/career_overlay.gd").new()
co_v5.open_career(data_v5)
Kit.check("旧档载入后生涯页正常显示(v5 升 v6:总局数/通关局数显示 0,既有钥石余额原样呈现)",
co_v5.visible and String(co_v5._total_label.text) == "0 局" \
and String(co_v5._cleared_label.text) == "0 局(三关全通)" \
and String(co_v5._keystone_badge.value_label.text) == "钥石 3")
co_v5.visible = false
co_v5.free()
# 内容级比较(核代码增补——对象引用比较恒真不设防):同型比内容(Texture 比纹理、Flat 比底色),异型即不同
func _sb_content_differs(a: StyleBox, b: StyleBox) -> bool:
if a is StyleBoxTexture and b is StyleBoxTexture:
return (a as StyleBoxTexture).texture != (b as StyleBoxTexture).texture
if a is StyleBoxFlat and b is StyleBoxFlat:
return (a as StyleBoxFlat).bg_color != (b as StyleBoxFlat).bg_color
return a != b
+36 -33
View File
@@ -13,7 +13,7 @@
> ——标签/cond/计数三对齐才算迁移完(合同 §五出口机检)。
> 维护义务:此后断言新增/删除/改判必须同步本清单(test-conventions §9.3 锚)。
## S01 _initialize(行 44–70 · 14 条 · 去向:单元测试 → tests/unit/data_config/rng_stream_boot_test.gd · 施工组 G12)
## S01 _initialize(行 44–70 · 14 条 · 去向:单元测试 → tests/unit/data_config/rng_stream_boot_test.gd · 施工组 G12;去向补正:转办行→cross_system_flow_test.gd,见维护记录)
| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet |
|---|---|---|---|---|---|---|
@@ -30,7 +30,7 @@
| 11 | 65 | "open_stream 全局计数器单调 +1 ×2" | rng.counter() == 2 | 开两条流后全局取号计数器恰为 2(单调加一,不跳不漏)。 | 迁单元测试(tests/unit/data_config/rng_stream_boot_test.gd) | |
| 12 | 66 | "首个取号 = scope 0(顺序确定)" | s0.seed == rng.derive_at(0) and s1.seed == rng.derive_at(1) | 首个与次个 open_stream 依次取全局 scope 0、1 的派生种子(取号顺序确定)。 | 迁单元测试(tests/unit/data_config/rng_stream_boot_test.gd) | |
| 13 | 69 | "new_adventure 重置全局计数器" | rng.counter() == 1 and s2.seed == rng.derive_at(0) | new_adventure 后计数器归 1 且新流首号=scope 0(会话重置语义)。 | 迁单元测试(tests/unit/data_config/rng_stream_boot_test.gd) | |
| 14 | 70 | "事件日志会话落盘(2026-09-27 统一流裁决)" | FileAccess.file_exists("user://logs/rng_log_4242.jsonl") | RNG 事件日志按会话落盘成文件(2026-09-27 统一流裁决的落地面)。 | 迁单元测试(tests/unit/data_config/rng_stream_boot_test.gd) | |
| 14 | 70 | "事件日志会话落盘(2026-09-27 统一流裁决)" | FileAccess.file_exists("user://logs/rng_log_4242.jsonl") | RNG 事件日志按会话落盘成文件(2026-09-27 统一流裁决的落地面)。 |迁集成测试(tests/integration/cross_system_flow_test.gd)| |
## S02 _split_tail(行 92–92 · 1 条 · 去向:系统测试 → tests/system_test.gd · 施工组 G1)
@@ -49,7 +49,7 @@
| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet |
|---|---|---|---|---|---|---|
| 18 | 132 | "V8 字符串 Callable 产品码零命中(实测 %d——方法引用换装闸)〔迭代27 T6〕" | callables == 0 | core/presentation/data 三目录产品码零字符串形式 Callable(self,…)——方法引用必须换装。 | 迁单元测试(tests/unit/contracts/source_guard_test.gd) | |
| 18 | 132 | "V8 字符串 Callable 产品码零命中(实测应 0——方法引用换装闸)〔迭代27 T6〕" | callables == 0 | core/presentation/data 三目录产品码零字符串形式 Callable(self,…)——方法引用必须换装。 | 迁单元测试(tests/unit/contracts/source_guard_test.gd) | |
| 19 | 134 | "V8 FeatsEngine 整文件退役(class_name 注销=文件删除)〔迭代27 T6〕" | not FileAccess.file_exists("res://core/feats_engine.gd") | core/feats_engine.gd 文件已删(FeatsEngine 退役=文件消失,不是注释掉)。 | 迁单元测试(tests/unit/contracts/source_guard_test.gd) | |
| 20 | 137 | "V8 死代码抽查·_attune_row 零残留〔迭代27 T6〕" | not bo.contains("func _attune_row") | 战斗编排器源码无 _attune_row 死函数残留。 | 迁单元测试(tests/unit/contracts/source_guard_test.gd) | |
| 21 | 140 | "V8 死代码抽查·attunement_counters 零残留〔迭代27 T6〕" | not sm.contains("func attunement_counters") | 存档管理器源码无 attunement_counters 死函数残留。 | 迁单元测试(tests/unit/contracts/source_guard_test.gd) | |
@@ -160,7 +160,8 @@
| 91 | 816 | "数值对(四级换代承袭):小GP 20/大GP 50 · 小增伤 +1/大 +2 · 小生命 +5/大 +10 · 小护甲 +1/大 +2" | int(_db.adventure.curio_value("gp", "common")) == 20 and int(_db.adventure.curio_value("gp", "excellent")) == 50 and int(_db.adventure.curio_value("attack", "common")) == 1 and int(_db.adventure.curio_value("attack", "excellent")) == 2 and int(_db.adventure.curio_value("max_hp", "common")) == 5 and int(_db.adventure.curio_value("max_hp", "excellent")) == 10 and int(_db.adventure.curio_value("armor", "common")) == 1 and int(_db.adventure.curio_value("armor", "excellent")) == 2 | 八类令牌件值对=正式承袭值(GP 20/50、增伤 1/2、生命 5/10、护甲 1/2)。 | 迁单元测试(tests/unit/data_config/table_structure_guard_test.gd) | |
| 92 | 821 | "恢复件 +6 当即回血(优秀档承袭)· AP +1(稀有档承袭)〔专长批 V14 换代〕" | int(_db.adventure.curio_value("heal", "excellent")) == 6 and int(_db.adventure.curio_value("ap", "rare")) == 1 | 恢复件优秀档 +6、AP 稀有档 +1(换代承袭值)。 | 迁单元测试(tests/unit/data_config/table_structure_guard_test.gd) | |
| 93 | 823 | "cap 占位在域(案②提前退役——99=实质无上限,实数归调参批)〔专长批 V8 形态面〕" | int(_db.adventure.curio_cap("gp")) == 99 and int(_db.adventure.curio_cap("attack")) == 99 | GP/攻击 cap 占位 99 在域(实质无上限口径)。 | 迁单元测试(tests/unit/data_config/table_structure_guard_test.gd) | |
| 94 | 825 | "同调位读数源=树同调位节点点亮数(迭代 21 T4 槽位收编——null 档求值 0;谓词表已退役)〔天赋树批 V14〕" | Attunement.slots(_db, null) == 0 | 同调位读数=树同调位节点点亮数(空档求值 0,谓词表已退役)。 | 迁单元测试(tests/unit/data_config/table_structure_guard_test.gd) | |
| 94 | 825 |"同调位读数 null 档早退分支求值 0(迭代 21 T4 槽位收编——谓词表已退役)〔天赋树批 V14·核代码增补〕"| Attunement.slots(_db, null) == 0 | 同调位读数=树同调位节点点亮数(空档求值 0,谓词表已退役)。 | 迁单元测试(tests/unit/data_config/table_structure_guard_test.gd) | |
| 8000 | — | "同调位读数源=树同调位节点点亮数(非空档——socket1 点亮 1 件计 1)〔天赋树批 V14·核代码增补〕" | Attunement.slots(_db, _TreeSaveMock.new()) == 1 | 同调位读数=非空档下树点亮同调位数(恒真对象引用比较已废,白盒定位补验)。 | 核代码增补(tests/unit/data_config/table_structure_guard_test.gd) | |
| 95 | 832 | "新目录 12 件全量落表(旧 12 行删除;装配词表=活行 12 件零重复)〔秘宝批 V1〕" | trows.size() == 12 and tid_set.size() == 12 | 同调目录 12 件全量落表且 id 零重复。 | 迁单元测试(tests/unit/data_config/table_structure_guard_test.gd) | |
| 96 | 843 | "命名锁定:12 件 item_id 与 display_name 逐件=正式名(C2 代号即名)〔秘宝批 V2 初筛〕" | names_ok | 12 件 item_id 与显示名逐件=正式定名(C2 代号即名)。 | 迁单元测试(tests/unit/data_config/table_structure_guard_test.gd) | |
| 97 | 871 | "逐行解锁方式与契约 8 分配一致(钥石 8 件带价格、条件 4 件带谓词行互斥)+三档 7/3/2〔秘宝批 V1〕" | alloc_ok and tier_map["common"] == 7 and tier_map["excellent"] == 3 and tier_map["rare"] == 2 | 逐行解锁方式与契约分配一致:8 件钥石价、4 件条件谓词互斥,档位 7/3/2。 | 迁单元测试(tests/unit/data_config/table_structure_guard_test.gd) | |
@@ -201,7 +202,7 @@
| 122 | 1023 | "奖励节点三选一 ≥1 次(L1 链必经 1 个;L2/L3 图可选路经若干——实际经停 %d)" ↩ % node_picks | node_picks >= 1 | 奖励节点三选一至少经停 1 次。 | 迁单元测试(tests/unit/core/adventure_feats_test.gd) | |
| 123 | 1028 | "奖励节点兑付回执与经停逐笔相等(context=reward_node——同账计数 %d/%d)" ↩ % [node_resolves, node_picks] | node_resolves == node_picks | 奖励节点兑付回执与经停数逐笔相等(同账计数)。 | 迁单元测试(tests/unit/core/adventure_feats_test.gd) | |
| 124 | 1078 | "三选一候选 2–3 枚且零重复(令牌洗牌不放回——kind+tier 复合身份)" | cand_ok | 三选一候选 2–3 枚且零重复(令牌洗牌不放回)。 | 迁单元测试(tests/unit/core/adventure_feats_test.gd) | |
| 125 | 1079 | "通关部分回血封顶(floor(上限×比例) 封顶缺口——血回满退役;购血容差窗口)" | heal_ok | 通关回血按生效上限比例封顶(血回满语义退役,含购血容差窗口)。 | 迁单元测试(tests/unit/core/adventure_feats_test.gd) | |
| 125 | 1079 |"通关部分回血封顶(floor(观测上限×比例) 封顶——购血观测值精确断言,核代码增补)"| heal_ok | 通关回血按生效上限比例封顶(血回满语义退役,含购血容差窗口)。 | 迁单元测试(tests/unit/core/adventure_feats_test.gd) | |
| 126 | 1080 | "钥石逐杀入账(首杀档位 + 累计逐位)" | ks_gain_ok | 钥石逐杀入账:首杀档位+累计前缀逐位一致。 | 迁单元测试(tests/unit/core/adventure_feats_test.gd) | |
| 127 | 1081 | "点选恰兑付选中一件(值=令牌档位表值;未选项零生效)" | resolve_ok | 点选恰兑付选中一件且值=表值(未选项零生效)。 | 迁单元测试(tests/unit/core/adventure_feats_test.gd) | |
| 128 | 1092 | "各战上限入参 ∈ [基础, 基础+3×件值+购血余量](覆盖值通道——双渠道共享上限 3;模拟档贪心购血合法叠加〔数值修复批 v3.4〕;首战=基础 %d)" ↩ % exp_base | seq_ok | 各战敌人上限入参落在[基础, 基础+件值+购血余量]窗口(覆盖值通道)。 | 迁单元测试(tests/unit/core/adventure_feats_test.gd) | |
@@ -245,7 +246,7 @@
| 166 | 1284 | "节点停点态兑付后清零(恰一次语义同构)" | a13._node_curio_pending.is_empty() | 节点停点态兑付后清零(恰一次语义)。 | 迁单元测试(tests/unit/core/adventure_feats_test.gd) | |
| 167 | 1286 | "放弃摘要:abandoned · 未打穿(已获大奖励不追回——同失败型结算)" | String(s10.get("result", "")) == "abandoned" and not bool(s10.get("cleared", true)) | 放弃摘要:abandoned、未打穿(已获不追回)。 | 迁单元测试(tests/unit/core/adventure_feats_test.gd) | |
## S14 _smoke_feats(行 1295–1637 · 71 条 · 去向:单元测试 → tests/unit/core/adventure_feats_test.gd · 施工组 G7)
## S14 _smoke_feats(行 1295–1637 · 71 条 · 去向:单元测试 → tests/unit/core/adventure_feats_test.gd · 施工组 G7;去向补正:转办行→cross_system_flow_test.gd,见维护记录)
| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet |
|---|---|---|---|---|---|---|
@@ -274,9 +275,9 @@
| 190 | 1402 | "A6 限购满不再触发(保底冻结)" | not a_t._feat_ticket_consider(rg_t) | 限购满后保底冻结不再触发。 | 迁单元测试(tests/unit/core/adventure_feats_test.gd) | |
| 191 | 1410 | "A6 概率面存在(chance=20% 200 展示内必出现——零触发=概率面失守)" | offered | 概率面存在:20% 概率 200 次展示内必出现(零触发=概率面失守)。 | 迁单元测试(tests/unit/core/adventure_feats_test.gd) | |
| 192 | 1412 | "relic 兑付:入持有集 + 牌库摘牌(选中踢池——池库存 %d→%d)" ↩ % [deck_relic, deck_relic - 1] | a_r.pick_curio("relic", "rare", "x3") and (a_r.feats_held() as Array).has("x3") and a_r.pool_stock("relic") == deck_relic - 1 | relic 兑付:入持有集+牌库摘牌(选中踢池)。 | 迁单元测试(tests/unit/core/adventure_feats_test.gd) | |
| 193 | 1420 | "断点附随件:relics 持有集入快照(迭代 12 T1 扩列)" | (bp_r.get("relics", []) as Array).has("x3") | 断点快照附随 relics 持有集(续玩还原面)。 | 迁单元测试(tests/unit/core/adventure_feats_test.gd) | |
| 194 | 1421 | "断点附随件:adventure 计数器账本键在案(T6/T7 扩列)" | int((bp_r.get("adventure_counters", {}) as Dictionary).size()) >= 0 | 断点快照附随 adventure 计数器账本键。 | 迁单元测试(tests/unit/core/adventure_feats_test.gd) | |
| 195 | 1426 | "断点余库:已兑付 relic 令牌逐枚还原(x3 零残留——续玩不重复获得)" | bp_deck_x3 == 0 | 断点余库逐枚还原:已兑付 relic 令牌零残留(续玩不重复获得)。 | 迁单元测试(tests/unit/core/adventure_feats_test.gd) | |
| 193 | 1420 | "断点附随件:relics 持有集入快照(迭代 12 T1 扩列)" | (bp_r.get("relics", []) as Array).has("x3") | 断点快照附随 relics 持有集(续玩还原面)。 |迁集成测试(tests/integration/cross_system_flow_test.gd)| |
| 194 | 1421 | "断点附随件:adventure 计数器账本键在案(T6/T7 扩列)" | bp_r.has("adventure_counters") | 断点快照附随 adventure 计数器账本键。 |迁集成测试(tests/integration/cross_system_flow_test.gd)| |
| 195 | 1426 | "断点余库:已兑付 relic 令牌逐枚还原(x3 零残留——续玩不重复获得)" | bp_deck_x3 == 0 | 断点余库逐枚还原:已兑付 relic 令牌零残留(续玩不重复获得)。 |迁集成测试(tests/integration/cross_system_flow_test.gd)| |
| 196 | 1449 | "双管镐:第 %d 次付费挖格返 1 AP(每第 2 次——mod_eq 预评估口径)" ↩ % (i + 1) | refund | 双管镐:每第 2 次付费挖格返 1 AP(mod_eq 预评估口径)。 | 迁单元测试(tests/unit/core/adventure_feats_test.gd) | |
| 197 | 1451 | "双管镐反例:第 %d 次不返(未满 2 次)" ↩ % (i + 1) | not refund | 双管镐反例:未满 2 次不返 AP。 | 迁单元测试(tests/unit/core/adventure_feats_test.gd) | |
| 198 | 1455 | "地质学家:首开新类型集 1 点〔专长批 V3〕" | orch._hits_of(geo_hits, "counter_add").size() == 1 | 地质学家:首开新资源类型积 1 点。 | 迁单元测试(tests/unit/core/adventure_feats_test.gd) | |
@@ -425,6 +426,7 @@
| 301 | 2228 | "奖励表 defense 正向行 effect_id = ac_up(显示名盾牌加护,权重/tier 不变)" | String(ac_reward.get("effect_id", "")) == "ac_up" and String(ac_reward.get("display_name", "")) == "盾牌加护" | 奖励表 defense 正向行 effect_id=ac_up(盾牌加护)。 | 迁单元测试(tests/unit/data_config/table_structure_guard_test.gd) | |
| 302 | 2230 | "effect_pool_map 无 defense_up/ac_up 键(减伤不入池——AC 不经乘区)" | not _db.settlement.effect_pool_map.has("defense_up") and not _db.settlement.effect_pool_map.has("ac_up") | 效果池映射无 defense_up/ac_up 键(AC 不经乘区)。 | 迁单元测试(tests/unit/data_config/table_structure_guard_test.gd) | |
| 303 | 2245 | "hero_defense 池 percent 行叠满不可触池底(墙最深%s > clamp_lo %d——单效果归零结构性不可达)" ↩ % [ wall_detail if wall_detail != "" else "(池内现无负向行)", int(_db.settlement.pool_clamp_lo)] | wall_ok | 免伤墙归零护栏:percent 行叠满不可触池底(BUG-0003 免疫类防回归)。 | 迁单元测试(tests/unit/data_config/table_structure_guard_test.gd) | |
| 8001 | — | "hero_defense 池映射数==0(绊线钉——红=已加映射,去核上方护栏是否已同步激活)" | hd_maps == 0 | 钉住 hero_defense 池空集现态(核代码增补)——有人加映射先红,提醒护栏转激活需核验。 | 核代码增补(tests/unit/data_config/table_structure_guard_test.gd) | |
| 304 | 2248 | "主角 armor_class 在域内(%d——暂定基准随表走)" ↩ % int(_db.hero.armor_class) | int(_db.hero.armor_class) >= 0 and int(_db.hero.armor_class) <= 30 | 主角 AC 在 [0,30] 域内(暂定基准随表走)。 | 迁单元测试(tests/unit/data_config/table_structure_guard_test.gd) | |
| 305 | 2265 | "敌方 AC 行值在已裁带宽内(杂鱼 0~5·重击手/重装手 8~9·Boss 8~12——迭代 5 返工批 v1 重投,越带行%s)" ↩ % [ ac_detail if ac_detail != "" else "无"] | ac_band | 敌方 AC 行值在裁带宽内(杂鱼 0~5/重击手 8~9/Boss 8~12)。 | 迁单元测试(tests/unit/data_config/table_structure_guard_test.gd) | |
| 306 | 2273 | "Boss AC 关卡台阶严格递增(%d < %d < %d)" ↩ % [int(boss_ac.get(1, 0)), int(boss_ac.get(2, 0)), int(boss_ac.get(3, 0))] | int(boss_ac.get(1, 0)) < int(boss_ac.get(2, 0)) and int(boss_ac.get(2, 0)) < int(boss_ac.get(3, 0)) | Boss AC 关卡台阶严格递增(L1<L2<L3)。 | 迁单元测试(tests/unit/data_config/table_structure_guard_test.gd) | |
@@ -656,18 +658,18 @@
| 467 | 3387 | "四情况 C:标空惩罚照常(归因×%d 负向兑付×%d)" ↩ % [miss_c, miss_neg_c] | miss_c >= 1 and miss_neg_c >= 1 | 场景 C:标空惩罚照常(归因+负向兑付)。 | 迁单元测试(tests/unit/battle_settlement/seal_polarity_test.gd) | |
| 468 | 3388 | "四情况 C:标中奖励照常(×%d)" ↩ % hit_c | hit_c >= 1 | 场景 C:标中奖励照常。 | 迁单元测试(tests/unit/battle_settlement/seal_polarity_test.gd) | |
## S36 _smoke_playtest_logger(行 3402–3435 · 8 条 · 去向:单元测试 → tests/unit/presentation/theme_projection_test.gd · 施工组 G11)
## S36 _smoke_playtest_logger(行 3402–3435 · 8 条 · 去向:单元测试 → tests/unit/presentation/theme_projection_test.gd · 施工组 G11;去向补正:转办行→cross_system_flow_test.gd,见维护记录)
| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet |
|---|---|---|---|---|---|---|
| 469 | 3402 | "会话文件开启(%s)" ↩ % path | path.contains("playtest_20260912") | 试玩日志会话文件开启(文件名含种子)。 | 迁单元测试(tests/unit/presentation/theme_projection_test.gd) | |
| 470 | 3403 | "快照战斗启动" | settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)}) | 快照战斗启动。 | 迁单元测试(tests/unit/presentation/theme_projection_test.gd) | |
| 471 | 3407 | "每结算一快照(%d 次结算 → %d 快照)" ↩ % [2, snaps.size()] | snaps.size() == 2 | 每结算恰一快照(2 结算→2 快照)。 | 迁单元测试(tests/unit/presentation/theme_projection_test.gd) | |
| 472 | 3414 | "快照字段齐备(缺 %s)" ↩ % str(missing) | missing.is_empty() | 快照必需字段齐备(12 键零缺)。 | 迁单元测试(tests/unit/presentation/theme_projection_test.gd) | |
| 473 | 3415 | "快照含英雄/经济/板序号(板 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") | 快照含英雄/经济/板序号。 | 迁单元测试(tests/unit/presentation/theme_projection_test.gd) | |
| 474 | 3418 | "快照 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)) | 快照 spawn_count 在位且等于单位数(无击杀轮全灭/空场可判别——Bug#1 回归)。 | 迁单元测试(tests/unit/presentation/theme_projection_test.gd) | |
| 475 | 3422 | "诊断转储文件生成(%s)" ↩ % diag | diag.contains("diag_") | 诊断转储文件生成。 | 迁单元测试(tests/unit/presentation/theme_projection_test.gd) | |
| 476 | 3435 | "JSONL 全部行可解析(%d 行 = session + 2 结算)" ↩ % lines | lines >= 3 and parse_ok | JSONL 全部行可解析(会话+2 结算)。 | 迁单元测试(tests/unit/presentation/theme_projection_test.gd) | |
| 469 | 3402 | "会话文件开启(%s)" ↩ % path | path.contains("playtest_20260912") | 试玩日志会话文件开启(文件名含种子)。 |迁集成测试(tests/integration/cross_system_flow_test.gd)| |
| 470 | 3403 | "快照战斗启动" | settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)}) | 快照战斗启动。 |迁集成测试(tests/integration/cross_system_flow_test.gd)| |
| 471 | 3407 | "每结算一快照(%d 次结算 → %d 快照)" ↩ % [2, snaps.size()] | snaps.size() == 2 | 每结算恰一快照(2 结算→2 快照)。 |迁集成测试(tests/integration/cross_system_flow_test.gd)| |
| 472 | 3414 | "快照字段齐备(缺 %s)" ↩ % str(missing) | missing.is_empty() | 快照必需字段齐备(12 键零缺)。 |迁集成测试(tests/integration/cross_system_flow_test.gd)| |
| 473 | 3415 | "快照含英雄/经济/板序号(板 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") | 快照含英雄/经济/板序号。 |迁集成测试(tests/integration/cross_system_flow_test.gd)| |
| 474 | 3418 | "快照 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)) | 快照 spawn_count 在位且等于单位数(无击杀轮全灭/空场可判别——Bug#1 回归)。 |迁集成测试(tests/integration/cross_system_flow_test.gd)| |
| 475 | 3422 | "诊断转储文件生成(%s)" ↩ % diag | diag.contains("diag_") | 诊断转储文件生成。 |迁集成测试(tests/integration/cross_system_flow_test.gd)| |
| 476 | 3435 | "JSONL 全部行可解析(%d 行 = session + 2 结算)" ↩ % lines | lines >= 3 and parse_ok | JSONL 全部行可解析(会话+2 结算)。 |迁集成测试(tests/integration/cross_system_flow_test.gd)| |
## S37 _smoke_encounter_roll(行 3448–3506 · 8 条 · 去向:单元测试 → tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 施工组 G6)
@@ -736,7 +738,7 @@
| 512 | 3753 | "构造触达:挖到安全格 ≥1(%d)" ↩ % int(probe["safe_events"]) | int(probe["safe_events"]) >= 1 | 构造触达:挖到安全格≥1。 | 迁单元测试(tests/unit/battle_settlement/seal_polarity_test.gd) | |
| 513 | 3770 | "挖安全格 → 只出正向兑付(效果行 ∈ 正向字典 + 每格 ≥1 行——%d 格)" ↩ % probe["safe_cells"].size() | safe_all_pos | 挖安全格只出正向兑付(效果行∈正向字典+每格≥1 行)。 | 迁单元测试(tests/unit/battle_settlement/seal_polarity_test.gd) | |
| 514 | 3787 | "挖危险格 → 只出负向兑付(效果行 ∈ 负向字典 + 每格 ≥1 行——%d 格)" ↩ % probe["mine_cells"].size() | mine_all_neg | 挖危险格只出负向兑付(效果行∈负向字典+每格≥1 行)。 | 迁单元测试(tests/unit/battle_settlement/seal_polarity_test.gd) | |
| 515 | 3796 | "封印命中 → 该危险格资源的正向兑付行(origin 指回危险格,效果 ∈ 正向字典)" | hit_ok | 封印命中→该危险格正向兑付行(origin 指回格,效果∈正向字典)。 | 迁单元测试(tests/unit/battle_settlement/seal_polarity_test.gd) | |
| 515 | 3796 | "封印命中 → 该危险格资源的正向兑付行(origin 指回危险格,效果 ∈ 正向字典;action 型按行值符号辨极性)" | hit_ok | 封印命中→该危险格正向兑付行(origin 指回格,效果∈正向字典)。 | 迁单元测试(tests/unit/battle_settlement/seal_polarity_test.gd) | |
| 516 | 3802 | "封印溢价配置在位(seal_hit_premium=125 provisional——形态 a 兑付溢价)" | prem_pct == 125 | 封印溢价配置在位(125 provisional)。 | 迁单元测试(tests/unit/battle_settlement/seal_polarity_test.gd) | |
| 517 | 3830 | "标中兑付溢价(×%d 行逐型=同型正向 ×%d/100——等号压线注记:δ=1.25 时条件二 E[标中]=1.25·E_dig 恰等成立)" ↩ % [prem_rows, prem_pct] | prem_rows >= 1 and prem_ok | 标中兑付逐型溢价=同型正向×1.25。 | 迁单元测试(tests/unit/battle_settlement/seal_polarity_test.gd) | |
| 518 | 3836 | "标空惩罚零溢价(负向直伤行=固定表值原值——溢价只挂标中正向)" | miss_flat_ok | 标空惩罚零溢价(负向直伤=固定表值原值)。 | 迁单元测试(tests/unit/battle_settlement/seal_polarity_test.gd) | |
@@ -1006,6 +1008,7 @@
| 707 | 5383 | "共享池账本 -1(跨渠道单点——taken %d → %d)" ↩ % [taken0, int(a8._curio_taken.get(kind, 0))] | int(a8._curio_taken.get(kind, 0)) == taken0 + 1 | 共享池账本 −1(跨渠道单点)。 | 迁单元测试(tests/unit/core/level_shop_economy_test.gd) | |
| 708 | 5393 | "A 链爬价 = ceil(base×b^(关序−1)×A^1)(进店第 2 购——%d)" ↩ % expect_p2 | int(second.get("price", -1)) == expect_p2 | A 链爬价=公式含 A^1(进店第 2 购)。 | 迁单元测试(tests/unit/core/level_shop_economy_test.gd) | |
| 709 | 5395 | "同 visit 第二购受理(A 链软闸在价不在限)" | r8.buy_pool_item(String(second.get("kind_id", ""))) | 同 visit 第二购受理(A 链软闸在价不在限)。 | 迁单元测试(tests/unit/core/level_shop_economy_test.gd) | |
| 8002 | — | "A 链爬价场景未构造(抬 GP 后货架仍无可购第二卡)——场景绑定失败" | false | 场景绑定失败显式红(核代码增补)——白盒抬 GP 后货架必无可购第二卡时红。 | 核代码增补(tests/unit/core/level_shop_economy_test.gd) | |
| 710 | 5403 | "切边件零入店(40 次重抽零 gp/aux——负交易切边+三轴 doctrine)" | not cut_seen | 切边件零入店(40 次重抽零 GP 袋/aux)。 | 迁单元测试(tests/unit/core/level_shop_economy_test.gd) | |
| 711 | 5414 | "库存尽剔牌库(attack 令牌耗尽——池消耗即上限跨渠道共库 reward→shop 向)" | not attack_seen | 库存尽剔牌库(池消耗即上限跨渠道共库)。 | 迁单元测试(tests/unit/core/level_shop_economy_test.gd) | |
| 712 | 5416 | "池空消费拒绝(handler 零副作用——买光即枯竭硬闸)" | not a8._consume_pool_purchase("attack", "common") and a8.pool_stock("attack") == stock_before | 池空消费拒绝(handler 零副作用买光即枯竭)。 | 迁单元测试(tests/unit/core/level_shop_economy_test.gd) | |
@@ -1279,7 +1282,7 @@
| 910 | 6505 | "V1 普通钮 normal/hover/disabled 均为石纹九宫格贴图样式盒" | n_sb is StyleBoxTexture and h_sb is StyleBoxTexture and d_sb is StyleBoxTexture | 普通钮三态为石纹九宫格贴图样式盒。 | 迁单元测试(tests/unit/presentation/theme_projection_test.gd) | |
| 911 | 6509 | "V1 普通钮 hover=modulate 提亮(乙案,异于常态 WHITE)" | h_mod != Color.WHITE and h_mod.r > 1.0 | 普通钮悬停=提亮(乙案,异于常态白)。 | 迁单元测试(tests/unit/presentation/theme_projection_test.gd) | |
| 912 | 6512 | "V1 普通钮 disabled=本族底 α0.45 幽灵化(甲案)" | is_equal_approx(d_mod.a, 0.45) and d_mod.r == 1.0 and d_mod.g == 1.0 | 普通钮禁用=本族底 α0.45 幽灵化(甲案)。 | 迁单元测试(tests/unit/presentation/theme_projection_test.gd) | |
| 913 | 6515 | "V1 普通钮 disabled≠pressed(旧错配已修正)" | d_sb != p_sb | 普通钮禁用≠按压(旧错配已修正)。 | 迁单元测试(tests/unit/presentation/theme_projection_test.gd) | |
| 913 | 6515 | "V1 普通钮 disabled≠pressed(旧错配已修正)" |_sb_content_differs(d_sb, p_sb)| 普通钮禁用≠按压(旧错配已修正)。 | 迁单元测试(tests/unit/presentation/theme_projection_test.gd) | |
| 914 | 6517 | "V1 普通钮 focus 样式盒为扁平环(StyleBoxFlat)" | f_sb is StyleBoxFlat | 普通钮焦点样式盒为扁平环。 | 迁单元测试(tests/unit/presentation/theme_projection_test.gd) | |
| 915 | 6519 | "V1 焦点环色=INK(与 font_focus_color 同源)" | fsb.border_color == uikit_s.INK | 焦点环色=INK(与焦点字色同源)。 | 迁单元测试(tests/unit/presentation/theme_projection_test.gd) | |
| 916 | 6520 | "V1 焦点环宽=2px" | is_equal_approx(fsb.border_width_left, 2.0) and is_equal_approx(fsb.border_width_right, 2.0) and is_equal_approx(fsb.border_width_top, 2.0) and is_equal_approx(fsb.border_width_bottom, 2.0) | 焦点环宽=2px(四边)。 | 迁单元测试(tests/unit/presentation/theme_projection_test.gd) | |
@@ -1289,13 +1292,13 @@
| 920 | 6535 | "V1 主钮 pressed/disabled=本族平涂(非石纹)" | ppn is StyleBoxFlat and pdd is StyleBoxFlat | 主钮按压/禁用=本族平涂(非石纹)。 | 迁单元测试(tests/unit/presentation/theme_projection_test.gd) | |
| 921 | 6536 | "V1 主钮 pressed=烛光黄深档(现行值不动)" | (ppn as StyleBoxFlat).bg_color == Color8(214, 172, 88) | 主钮按压=烛光黄深档(现行值)。 | 迁单元测试(tests/unit/presentation/theme_projection_test.gd) | |
| 922 | 6537 | "V1 主钮 disabled=本族底 α0.45 幽灵化" | is_equal_approx((pdd as StyleBoxFlat).bg_color.a, 0.45) | 主钮禁用=本族底 α0.45 幽灵化。 | 迁单元测试(tests/unit/presentation/theme_projection_test.gd) | |
| 923 | 6538 | "V1 主钮 disabled≠pressed" | pdd != ppn | 主钮禁用≠按压。 | 迁单元测试(tests/unit/presentation/theme_projection_test.gd) | |
| 923 | 6538 | "V1 主钮 disabled≠pressed" |_sb_content_differs(ppn, pdd)| 主钮禁用≠按压。 | 迁单元测试(tests/unit/presentation/theme_projection_test.gd) | |
| 924 | 6539 | "V1 主钮 hover=提亮档(承现行不动)" | (phv as StyleBoxFlat).bg_color == uikit_s.CANDLE.lightened(0.06) | 主钮悬停=提亮档(承现行)。 | 迁单元测试(tests/unit/presentation/theme_projection_test.gd) | |
| 925 | 6544 | "V1 危险钮 %s 样式盒非空" ↩ % st | db.has_theme_stylebox_override(st) | 危险钮五态样式盒逐态非空。 | 迁单元测试(tests/unit/presentation/theme_projection_test.gd) | |
| 926 | 6547 | "V1 危险钮 pressed/disabled=本族平涂(非石纹,旧错配已修正)" | dpr is StyleBoxFlat and dds is StyleBoxFlat | 危险钮按压/禁用=本族平涂。 | 迁单元测试(tests/unit/presentation/theme_projection_test.gd) | |
| 927 | 6548 | "V1 危险钮 pressed=深红深档(暗于常态)" | (dpr as StyleBoxFlat).bg_color.r < 180.0 / 255.0 | 危险钮按压=深红深档(暗于常态)。 | 迁单元测试(tests/unit/presentation/theme_projection_test.gd) | |
| 928 | 6549 | "V1 危险钮 disabled=本族底 α0.45 幽灵化" | is_equal_approx((dds as StyleBoxFlat).bg_color.a, 0.45) | 危险钮禁用=α0.45 幽灵化。 | 迁单元测试(tests/unit/presentation/theme_projection_test.gd) | |
| 929 | 6550 | "V1 危险钮 disabled≠pressed" | dds != dpr | 危险钮禁用≠按压。 | 迁单元测试(tests/unit/presentation/theme_projection_test.gd) | |
| 929 | 6550 | "V1 危险钮 disabled≠pressed" |_sb_content_differs(dpr, dds)| 危险钮禁用≠按压。 | 迁单元测试(tests/unit/presentation/theme_projection_test.gd) | |
| 930 | 6555 | "V2 红线维持 Button 类型词条零接触" | not th.get_type_list().has("Button") | 主题红线维持:Button 类型词条零接触。 | 迁单元测试(tests/unit/presentation/theme_projection_test.gd) | |
| 931 | 6562 | "V2 红线维持 focus 族词条零定义%s" ↩ % ("" if focus_hits == "" else "(违例:" + focus_hits + ")") | focus_hits == "" | 主题红线维持:focus 族词条零定义。 | 迁单元测试(tests/unit/presentation/theme_projection_test.gd) | |
@@ -1352,7 +1355,7 @@
| 963 | 6673 | "V6 浮窗转储钮外发方法在位(纯视图边界)" | dbg_src.contains("_emit_diagnose_requested") | 浮窗转储钮外发方法在位(纯视图边界)。 | 迁单元测试(tests/unit/presentation/theme_projection_test.gd) | |
| 964 | 6674 | "V6 浮窗诊断转储钮在位(玩家画面角落钮清零后唯一入口)" | dbg_src.contains('UiKit.button("转储")') | 浮窗诊断转储钮在位(唯一入口)。 | 迁单元测试(tests/unit/presentation/theme_projection_test.gd) | |
## S76 _smoke_save(行 6713–6977 · 70 条 · 去向:单元测试 → tests/unit/core/save_manager_domain_test.gd · 施工组 G10)
## S76 _smoke_save(行 6713–6977 · 70 条 · 去向:单元测试 → tests/unit/core/save_manager_domain_test.gd · 施工组 G10;去向补正:转办行→cross_system_flow_test.gd,见维护记录)
| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet |
|---|---|---|---|---|---|---|
@@ -1384,16 +1387,16 @@
| 990 | 6795 | "域界干净——meta 域无局内资源键(发现 %s)" ↩ % str(leaked) | leaked.is_empty() | 域界干净——meta 域无局内资源键。 | 迁单元测试(tests/unit/core/save_manager_domain_test.gd) | |
| 991 | 6803 | "摘要 cap 10(12 份 → 10 条)" | arr.size() == 10 | 摘要上限 10(12 份截断)。 | 迁单元测试(tests/unit/core/save_manager_domain_test.gd) | |
| 992 | 6804 | "最新在前(头 seed 11 / 尾 seed 2)" | int(arr[0].get("master_seed", -1)) == 11 and int(arr[9].get("master_seed", -1)) == 2 | 摘要最新在前。 | 迁单元测试(tests/unit/core/save_manager_domain_test.gd) | |
| 993 | 6815 | "摘要集成:冒险驱动满配通关(seed 42)" | String(st9.get("result", "")) == "completed" | 摘要集成:真实局满配通关。 | 迁单元测试(tests/unit/core/save_manager_domain_test.gd) | |
| 994 | 6816 | "集成钥石余额 = ΣBoss 首杀行值(%d——逐杀掉落全源)" ↩ % f9.keystone_balance() | f9.keystone_balance() == ks_run | 集成钥石余额=ΣBoss 首杀行值。 | 迁单元测试(tests/unit/core/save_manager_domain_test.gd) | |
| 995 | 6817 | "集成通关次数表全 1" | f9.clear_count(1) == 1 and f9.clear_count(2) == 1 and f9.clear_count(3) == 1 | 集成通关次数表全 1。 | 迁单元测试(tests/unit/core/save_manager_domain_test.gd) | |
| 996 | 6818 | "集成三关首杀标记齐" | f9.boss_first_kill_marked(1) and f9.boss_first_kill_marked(2) and f9.boss_first_kill_marked(3) | 集成三关首杀标记齐。 | 迁单元测试(tests/unit/core/save_manager_domain_test.gd) | |
| 997 | 6820 | "集成 recent 1 条且 seed 42" | f9.recent_summaries().size() == 1 and int(f9.recent_summaries()[0].get("master_seed", -1)) == 42 | 集成摘要 1 条且种子正确。 | 迁单元测试(tests/unit/core/save_manager_domain_test.gd) | |
| 998 | 6824 | "第二局后通关次数全 2" | f9.clear_count(1) == 2 and f9.clear_count(2) == 2 and f9.clear_count(3) == 2 | 第二局后通关次数全 2。 | 迁单元测试(tests/unit/core/save_manager_domain_test.gd) | |
| 999 | 6825 | "第二局后余额翻倍(2 × Σ首杀——模拟驱动恒首杀口径)" | f9.keystone_balance() == 2 * ks_run | 第二局后余额翻倍。 | 迁单元测试(tests/unit/core/save_manager_domain_test.gd) | |
| 1000 | 6826 | "集成 recent 长度 2" | f9.recent_summaries().size() == 2 | 集成摘要长度 2。 | 迁单元测试(tests/unit/core/save_manager_domain_test.gd) | |
| 1001 | 6827 | "集成写盘成功" | f9.write_to_disk() | 集成写盘成功。 | 迁单元测试(tests/unit/core/save_manager_domain_test.gd) | |
| 1002 | 6829 | "集成写盘读回全量保持" | 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) | 集成写盘读回全量保持。 | 迁单元测试(tests/unit/core/save_manager_domain_test.gd) | |
| 993 | 6815 | "摘要集成:冒险驱动满配通关(seed 42)" | String(st9.get("result", "")) == "completed" | 摘要集成:真实局满配通关。 |迁集成测试(tests/integration/cross_system_flow_test.gd)| |
| 994 | 6816 | "集成钥石余额 = ΣBoss 首杀行值(%d——逐杀掉落全源)" ↩ % f9.keystone_balance() | f9.keystone_balance() == ks_run | 集成钥石余额=ΣBoss 首杀行值。 |迁集成测试(tests/integration/cross_system_flow_test.gd)| |
| 995 | 6817 | "集成通关次数表全 1" | f9.clear_count(1) == 1 and f9.clear_count(2) == 1 and f9.clear_count(3) == 1 | 集成通关次数表全 1。 |迁集成测试(tests/integration/cross_system_flow_test.gd)| |
| 996 | 6818 | "集成三关首杀标记齐" | f9.boss_first_kill_marked(1) and f9.boss_first_kill_marked(2) and f9.boss_first_kill_marked(3) | 集成三关首杀标记齐。 |迁集成测试(tests/integration/cross_system_flow_test.gd)| |
| 997 | 6820 | "集成 recent 1 条且 seed 42" | f9.recent_summaries().size() == 1 and int(f9.recent_summaries()[0].get("master_seed", -1)) == 42 | 集成摘要 1 条且种子正确。 |迁集成测试(tests/integration/cross_system_flow_test.gd)| |
| 998 | 6824 | "第二局后通关次数全 2" | f9.clear_count(1) == 2 and f9.clear_count(2) == 2 and f9.clear_count(3) == 2 | 第二局后通关次数全 2。 |迁集成测试(tests/integration/cross_system_flow_test.gd)| |
| 999 | 6825 | "第二局后余额翻倍(2 × Σ首杀——模拟驱动恒首杀口径)" | f9.keystone_balance() == 2 * ks_run | 第二局后余额翻倍。 |迁集成测试(tests/integration/cross_system_flow_test.gd)| |
| 1000 | 6826 | "集成 recent 长度 2" | f9.recent_summaries().size() == 2 | 集成摘要长度 2。 |迁集成测试(tests/integration/cross_system_flow_test.gd)| |
| 1001 | 6827 | "集成写盘成功" | f9.write_to_disk() | 集成写盘成功。 |迁集成测试(tests/integration/cross_system_flow_test.gd)| |
| 1002 | 6829 | "集成写盘读回全量保持" | 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) | 集成写盘读回全量保持。 |迁集成测试(tests/integration/cross_system_flow_test.gd)| |
| 1003 | 6835 | "断点:新档无断点(合法空态——「继续上局」隐藏)" | not fbp.has_breakpoint() | 断点:新档无断点(合法空态)。 | 迁单元测试(tests/unit/core/save_manager_domain_test.gd) | |
| 1004 | 6846 | "断点:写入+写盘成功" | fbp.has_breakpoint() and fbp.write_to_disk() | 断点写入+写盘成功。 | 迁单元测试(tests/unit/core/save_manager_domain_test.gd) | |
| 1005 | 6848 | "断点:读回在档" | rb.load_from_disk() and rb.has_breakpoint() | 断点读回在档。 | 迁单元测试(tests/unit/core/save_manager_domain_test.gd) | |
-291
View File
@@ -1,291 +0,0 @@
# 批 B 变异统一台账(机器生成)
> 生成命令:`godot --headless --path game --script res://tests/mutation/mutation_engine.gd -- --all --snap=seal_polarity=res://tests/.gates/mut_snapshots/seal_polarity_test.gd(23 套分批跑齐:system/TSG/board/roll/seal;board_generator 与 roll 尾套带 --excl-func/--excl-line 毒候选隔离与 --budget 收刀,各批命令见实现报告)`
> 生成时间:2026-10-23T13:33:47 · 引擎:游戏环境启动一次,逐条执行「内存内改坏 → 重跑该断言所在套函数 → 验 FAIL → 撤销 → 记录」,全程不改任何文件。
> 三武器:table=已加载 Resource 内存字段改写 · inject=生产脚本源码内存变换+reload 原地重编译(零触盘)· state=对象状态篡改(长活成员/静态与初值/autoload 树状态)。
> 中间结果 JSON 在 `user://mutation_work/results/`(仓库外);本文件为唯一持久产出。
## 一、逐条记录
| 宿主 | 套 | # | 标签 | 武器 | 改坏点 | 结果 | 复验轮耗时 | 连带红 |
|---|---|---|---|---|---|---|---|---|
| system_test | split_tail | 0 | false | state | ConfigDB:-1 tree_rename / [状态·树] autoload 节点 /root/ConfigDB 改名(装配链就绪护栏击穿) | 验红 | 13ms | 0 |
| system_test | smoke_scene_split | 1 | menu.start_overlay != null and menu.lvbtn != null and menu.resumebtn != null and menu.campbtn !=… | | | 异常 | — | — |
| system_test | smoke_scene_split | 2 | menu.start_overlay.visible | | | 异常 | — | — |
| system_test | smoke_scene_split | 3 | menu.mouse_filter == Control.MOUSE_FILTER_IGNORE | | | 异常 | — | — |
| system_test | smoke_scene_split | 4 | camp.camp_overlay != null | | | 异常 | — | — |
| system_test | smoke_scene_split | 5 | not camp.camp_overlay.visible | | | 异常 | — | — |
| system_test | smoke_scene_split | 6 | adv.battle_root != null and adv.stage != null and adv.board_zone != null and adv.board_view != n… | | | 异常 | — | — |
| system_test | smoke_scene_split | 7 | top == main._start_overlay | inject | main.gd:198 bool true→false / main.gd:199 bool true→false [_start_overlay.visible = true → _star… | 验红 | 221ms | 0 |
| system_test | smoke_scene_split | 8 | main._menu_screen.get_index() > main._adventure_screen.get_index() | inject | main.gd:467 num 1→0 / main.gd:468 num 1→0 [slot += 1 → slot += 0] | 验红 | 204ms | 2 |
| system_test | smoke_scene_split | 9 | chain_ok | inject | main.gd:222 asgn 置null / main.gd:223 asgn 置null [info_panel = _adventure_screen.info_panel → inf… | 验红 | 224ms | 0 |
| system_test | smoke_scene_split | 10 | main._start_overlay == main._menu_screen.start_overlay and main.camp_overlay == main._camp_scree… | inject | main.gd:218 asgn 置null / main.gd:219 asgn 置null [_start_overlay = _menu_screen.start_overlay → _… | 验红 | 221ms | 1 |
| system_test | smoke_scene_split | 11 | main.run_scope != null and not main.run_scope.is_adventure | inject | main.gd:196 bool false→true / main.gd:197 bool false→true [_begin_session(false) # 闲时会话(gp_led… | 验红 | 214ms | 0 |
| system_test | smoke_boot_chain | 12 | db.is_ready and db.hero != null | | | 异常 | — | — |
| system_test | smoke_boot_chain | 13 | AudioServer.is_bus_mute(master) == (ss.volume_linear <= 0.001) and is_equal_approx(AudioServer.g… | | | 异常 | — | — |
| system_test | smoke_boot_chain | 14 | ui_kit.motion_scale == (0.5 if ss.reduced_motion else 1.0) | | | 异常 | — | — |
| system_test | smoke_boot_chain | 15 | main._start_overlay.visible | inject | main.gd:198 bool true→false / main.gd:199 bool true→false [_start_overlay.visible = true → _star… | 验红 | 121ms | 2 |
| system_test | smoke_boot_chain | 16 | not sm.has_breakpoint() and not main._resume_btn.visible | | | 异常 | — | — |
| system_test | smoke_boot_chain | 17 | sm.active_slot() == 0 and not sm.has_write_target() | inject | save_manager.gd:358 bool false→true / save_manager.gd:359 bool false→true [return false → return… | 验红 | 133ms | 1 |
| system_test | smoke_boot_chain | 18 | sm.keystone_balance() == 0 and not sm.has_breakpoint() and sm.adventure_total() == 0 | | | 异常 | — | — |
| system_test | smoke_boot_chain | 19 | not sm.write_to_disk() | inject | save_manager.gd:358 bool false→true / save_manager.gd:359 bool false→true [return false → return… | 验红 | 131ms | 1 |
| system_test | smoke_boot_chain | 20 | not (FileAccess.file_exists("user://save.json") and not FileAccess.file_exists(sm.slot_path(1))) | | | 异常 | — | — |
| system_test | smoke_boot_chain | 21 | not sm.has_breakpoint() and not main._resume_btn.visible | | | 异常 | — | — |
| system_test | smoke_boot_chain | 22 | sm.has_breakpoint() and main._resume_btn.visible | inject | main.gd:533 op ==→!= / main.gd:534 op ==→!= [if _resume_btn == null: → if _resume_btn != null:] | 验红 | 122ms | 2 |
| system_test | smoke_boot_chain | 23 | not main._resume_btn.visible | | | 异常 | — | — |
| system_test | smoke_boot_chain | 24 | main.run_scope != null and not main.run_scope.is_adventure and main.run_scope.is_inside_tree() | inject | main.gd:171 op !=→== / main.gd:172 op !=→== [if run_scope != null: → if run_scope == null:] | 验红 | 138ms | 2 |
| system_test | smoke_boot_chain | 25 | not main.debug_overlay.visible | | | 异常 | — | — |
| system_test | smoke_boot_chain | 26 | main.debug_badge != null and main.debug_badge.visible | inject | main.gd:199 bool true→false / main.gd:200 bool true→false [_debug_entry = true # 调试入口门控写死=true… | 验红 | 122ms | 2 |
| system_test | smoke_run_lifecycle | 27 | main.run_scope != null and main.run_scope.is_adventure and main.run_scope.adventure != null | inject | main.gd:492 if 取反 / main.gd:493 if 取反 [if save_manager.has_breakpoint(): → if not (save_manager.… | 验红 | 138ms | 0 |
| system_test | smoke_run_lifecycle | 28 | main.run_scope.hints_shown.is_empty() | | | 异常 | — | — |
| system_test | smoke_run_lifecycle | 29 | main.run_scope.adventure.abandon() | | | 异常 | — | — |
| system_test | smoke_run_lifecycle | 30 | finished.done | inject | main.gd:11 key 错位:"/root/RngService" / main.gd:12 key 错位:"/root/RngService" [const AUTOLOAD_PATH… | 验红 | 331ms | 3 |
| system_test | smoke_run_lifecycle | 31 | main._end_overlay.is_open() | inject | main.gd:11 key 错位:"/root/RngService" / main.gd:12 key 错位:"/root/RngService" [const AUTOLOAD_PATH… | 验红 | 333ms | 3 |
| system_test | smoke_run_lifecycle | 32 | not main.run_scope.is_adventure and main.run_scope.adventure == null and main.run_scope.runner =… | inject | main.gd:726 bool false→true / main.gd:727 bool false→true [_begin_session(false) → _begin_sessio… | 验红 | 141ms | 1 |
| system_test | smoke_run_lifecycle | 33 | main.run_scope.hints_shown.is_empty() and not main.run_scope.boss_active and main.run_scope.last… | inject | main.gd:712 if 取反 / main.gd:713 if 取反 [if _bonus.resume_if_pending(): # 三选一停点在途恢复入口… → if not … | 验红 | 141ms | 2 |
| system_test | smoke_run_lifecycle | 34 | main._start_overlay.visible | inject | main.gd:728 bool true→false / main.gd:729 bool true→false [_start_overlay.visible = true → _star… | 验红 | 153ms | 1 |
| system_test | smoke_run_lifecycle | 35 | not sm.has_breakpoint() | | | 异常 | — | — |
| system_test | smoke_run_lifecycle | 36 | main.run_scope.is_adventure and main.run_scope.adventure != null and main.run_scope.hints_shown.… | inject | main.gd:174 asgn 置null / main.gd:175 asgn 置null [run_scope.is_adventure = adventure_session → ru… | 验红 | 212ms | 2 |
| system_test | smoke_run_lifecycle | 37 | not main._start_overlay.visible | inject | main.gd:498 bool false→true / main.gd:499 bool false→true [_start_overlay.visible = false → _sta… | 验红 | 141ms | 1 |
| system_test | smoke_lifecycle_matrix | 39 | main.run_scope.settlement == null or not main.run_scope.settlement.is_battle_live() | | | 异常 | — | — |
| system_test | smoke_lifecycle_matrix | 40 | main.run_scope.adventure.abandon() | | | 异常 | — | — |
| system_test | smoke_lifecycle_matrix | 41 | finished.done and main._end_overlay.is_open() | | | 异常 | — | — |
| system_test | smoke_lifecycle_matrix | 42 | main.run_scope != old_scope and main.run_scope.is_inside_tree() | inject | main.gd:716 bool false→true / main.gd:717 bool false→true [_settlement_stop_pending = false → _s… | 验红 | 6481ms | 2 |
| system_test | smoke_lifecycle_matrix | 43 | not is_instance_valid(old_scope) | inject | main.gd:716 bool false→true / main.gd:717 bool false→true [_settlement_stop_pending = false → _s… | 验红 | 6341ms | 2 |
| system_test | smoke_lifecycle_matrix | 44 | false | | | 异常 | — | — |
| system_test | smoke_lifecycle_matrix | 45 | main.run_scope.adventure != null and main.run_scope.adventure != first_adv | | | 异常 | — | — |
| system_test | smoke_lifecycle_matrix | 46 | main.run_scope.adventure.abandon() | | | 异常 | — | — |
| system_test | smoke_lifecycle_matrix | 47 | main._resume_btn.visible | inject | main.gd:533 op ==→!= / main.gd:534 op ==→!= [if _resume_btn == null: → if _resume_btn != null:] | 验红 | 4864ms | 0 |
| system_test | smoke_lifecycle_matrix | 48 | main.run_scope != idle_scope and main.run_scope.is_inside_tree() | inject | main.gd:544 if 去not / main.gd:545 if 去not [if not save_manager.has_breakpoint(): → if save_manag… | 验红 | 2963ms | 1 |
| system_test | stage_faces | 38 | bad.is_empty() | inject | main.gd:218 asgn 置null / main.gd:219 asgn 置null [_start_overlay = _menu_screen.start_overlay → _… | 验红 | 307ms | 0 |
| table_structure_guard | smoke_ac_swap_guard | 21 | "状态表 defense_up 行已退役(无双轨)" | table | status_effects.rows.effect_id=defense_up:-1 table 字面量插行 / 表插行 status_effects.rows:effect_id=defe… | 验红 | 5ms | 0 |
| table_structure_guard | smoke_ac_swap_guard | 22 | "ac_up 行在位(hero · +1 ac · 只续时不叠层——2026-09-20 主角增益续时批)" | table | status_effects.rows[1].effect_id:-1 table 删键 / 表行删键 status_effects.rows[1].effect_id | 验红 | 5ms | 0 |
| table_structure_guard | smoke_ac_swap_guard | 23 | "奖励表 defense 正向行 effect_id = ac_up(显示名盾牌加护,权重/tier 不变)" | table | reward.rows[1].cell_type:-1 table 删键 / 表行删键 reward.rows[1].cell_type | 验红 | 5ms | 0 |
| table_structure_guard | smoke_ac_swap_guard | 24 | "effect_pool_map 无 defense_up/ac_up 键(减伤不入池——AC 不经乘区)" | table | settlement.effect_pool_map.defense_up:-1 table 字典加键 / 字典加键 settlement.effect_pool_map:defense_up… | 验红 | 5ms | 0 |
| table_structure_guard | smoke_ac_swap_guard | 25 | "hero_defense 池 percent 行叠满不可触池底(墙最深%s > clamp_lo %d——单效果归零结构性不可达)" % [ wall_detail if wall_deta… | | | 异常 | — | — |
| table_structure_guard | smoke_ac_swap_guard | 26 | "主角 armor_class 在域内(%d——暂定基准随表走)" % int(_db.hero.armor_class) | table | hero.armor_class:-1 table_set / 表改写 hero.armor_class: 10 → 100 | 验红 | 5ms | 0 |
| table_structure_guard | smoke_ac_swap_guard | 27 | "敌方 AC 行值在已裁带宽内(杂鱼 0~5·重击手/重装手 8~9·Boss 8~12——迭代 5 返工批 v1 重投,越带行%s)" % [ ac_detail if ac_detail … | table | enemy.rows[4].enemy_id:-1 table_set / 表行改写 enemy.rows[4].enemy_id: ogre_chief → ogre_chief_x | 验红 | 5ms | 0 |
| table_structure_guard | smoke_ac_swap_guard | 28 | "Boss AC 关卡台阶严格递增(%d < %d < %d)" % [int(boss_ac.get(1, 0)), int(boss_ac.get(2, 0)), int(boss_ac.… | table | enemy.rows[12].level_index:-1 table 删键 / 表行删键 enemy.rows[12].level_index | 验红 | 5ms | 0 |
| table_structure_guard | smoke_ac_swap_guard | 29 | "熊地精首遇带(1,battle,%d)降档锚定(hp 22 · ac 2 · 池 attack6/charge2/heavy2 零改动——迭代 24 N1;首遇带数据驱动定位)〔fv21〕"… | table | enemy.rows[2].level_index:-1 table 删键 / 表行删键 enemy.rows[2].level_index | 验红 | 5ms | 1 |
| table_structure_guard | smoke_ac_swap_guard | 30 | "第一关相邻深度带总量比 ≤ 2.5(12/10/22/35/72——迭代 24 N1 曲线护栏,带键升序;越带%s)" % [ curve_detail if curve_detail !=… | table | enemy.rows[2].level_index:-1 table 删键 / 表行删键 enemy.rows[2].level_index | 验红 | 5ms | 1 |
| table_structure_guard | smoke_ac_swap_guard | 31 | "掷点参数拍板值在位(d20 / nat20 暴击 ×200%% / 下限 1 / attack_bonus 0 / armor_roll 流)" | table | settlement.roll_faces:-1 table_set / 表改写 settlement.roll_faces: 20 → 21 | 验红 | 5ms | 0 |
| table_structure_guard | smoke_ac_swap_guard | 32 | "档位表拍板值在位(缺口 1-3 → ×0.75 · 4-7 → ×0.5 · 8+ → ×0.25——无零档)" | table | settlement.roll_tier_bounds:-1 table 删行 / 表删行 settlement.roll_tier_bounds[0] | 验红 | 5ms | 0 |
| table_structure_guard | smoke_enemy_bands | 17 | "敌人表 20 行且键序 = 迁移表序((L,row_kind,depth_band,enemy_id) 逐行比对——文件行序不变)〔fv11〕" | table | enemy.rows[0].depth_band:-1 table 删键 / 表行删键 enemy.rows[0].depth_band | 验红 | 5ms | 0 |
| table_structure_guard | smoke_enemy_bands | 18 | "hobgoblin 行在 (1,battle,3) 留任随迁(缺口行不删不补——补齐归敌种内容批)〔fv12〕〔fv25〕" | table | enemy.rows[3].depth_band:-1 table 删键 / 表行删键 enemy.rows[3].depth_band | 验红 | 5ms | 1 |
| table_structure_guard | smoke_enemy_bands | 19 | "hobgoblin 缺口登记随行(表文件注释在位——缺口不静默)〔fv25〕" | | | 异常 | — | — |
| table_structure_guard | smoke_enemy_bands | 20 | "三关 Boss 行各恰一行且 row_kind=boss、band=图 Boss 节点深度(%d/%d/%d%s)〔fv13〕" % [ _boss_depth(1), _boss_dept… | table | enemy.rows[4].depth_band:-1 table 删键 / 表行删键 enemy.rows[4].depth_band | 验红 | 5ms | 1 |
| table_structure_guard | smoke_variation_framework | 33 | "变异清单 6 行(荆棘丛/藤蔓缠绕/岩浆/烬覆 + 落石/矿尘——首批类型定稿批 + 第三关内容批;贫矿脉 2026-10-16 用户令屏蔽删行)" | table | variation.rows:-1 table 复插行 / 表复插行 variation.rows[0] → 表首 | 验红 | 5ms | 3 |
| table_structure_guard | smoke_variation_framework | 34 | "变异表校验零错误(6 行全过域与跨字段互锁——含 scope=global 三互锁负样本)" | table | variation.rows[0].variant_id:-1 table 删键 / 表行删键 variation.rows[0].variant_id | 验红 | 21ms | 0 |
| table_structure_guard | smoke_variation_framework | 35 | "L1 dungeon 候选池 = 荆棘丛+藤蔓缠绕(unlock + board_types 双闸)" | table | variation.rows:-1 table 复插行 / 表复插行 variation.rows[0] → 表首 | 验红 | 5ms | 3 |
| table_structure_guard | smoke_variation_framework | 36 | "L2 underdark 候选池 = 岩浆+烬覆(第二关解锁)" | table | variation.rows:-1 table 复插行 / 表复插行 variation.rows[2] → 表首 | 验红 | 5ms | 2 |
| table_structure_guard | smoke_variation_framework | 37 | "unlock 门控:L1 不进 underdark 行(岩浆/烬覆第二关解锁)" | table | variation.rows[2].unlock_level:-1 table_set / 表行改写 variation.rows[2].unlock_level: 2 → 1 | 验红 | 5ms | 0 |
| table_structure_guard | smoke_variation_framework | 38 | "board_types 门控:L2 dungeon 板池 = 荆棘丛+藤蔓缠绕(解锁累积;岩浆/烬覆被 board_types ∅ 交集排除)" | table | variation.rows:-1 table 复插行 / 表复插行 variation.rows[0] → 表首 | 验红 | 5ms | 3 |
| table_structure_guard | smoke_variation_framework | 39 | "清单外 variant_id 返回空(读取通道在位)" | table | variation.rows.variant_id=nonexistent:-1 table 字面量插行 / 表插行 variation.rows:variant_id=nonexistent… | 验红 | 5ms | 3 |
| table_structure_guard | smoke_variation_framework | 40 | "跨字段互锁负样本①:flat_damage + placement=any 被拒(只出现在未打开的格子)" | inject | variation_table.gd:28 key 错位:"variant_id" / variation_table.gd:29 key 错位:"variant_id" [var vid: … | 验红 | 5ms | 1 |
| table_structure_guard | smoke_variation_framework | 41 | "跨字段互锁负样本②:hide_number + trigger≠on_number_shown 被拒" | inject | variation_table.gd:28 key 错位:"variant_id" / variation_table.gd:29 key 错位:"variant_id" [var vid: … | 验红 | 5ms | 1 |
| table_structure_guard | smoke_difficulty_table | 42 | "DifficultyTable MVP 空表加载(结构位)" | table | difficulty.rows:-1 table 空表插行 / 空表插行 difficulty.rows(合成行灭 is_empty) | 验红 | 5ms | 0 |
| table_structure_guard | smoke_difficulty_table | 43 | "spawn 受理" | inject | unit_container.gd:37 key 错位:"battle" / unit_container.gd:38 key 错位:"battle" [hero_attack_type: S… | 验红 | 69ms | 0 |
| table_structure_guard | smoke_difficulty_table | 44 | "空表零修饰(hp=%d atk=%d = 行值 %d/%d)" % [int(unit.hp), int(unit.atk), int(d0.hp), int(d0.atk)] | inject | unit_container.gd:57 num 1.0→2.0 / unit_container.gd:58 num 1.0→2.0 [var hp_mult: float = float(… | 验红 | 5ms | 0 |
| table_structure_guard | smoke_difficulty_table | 45 | "修饰后 spawn 受理" | inject | unit_container.gd:116 bool true→false / unit_container.gd:117 bool true→false [return true → ret… | 验红 | 5ms | 2 |
| table_structure_guard | smoke_difficulty_table | 46 | "hp 修饰 ×1.5(%d→%d,max_hp 同步)" % [int(d0.hp), int(g2.hp)] | inject | unit_container.gd:57 key 错位:"hp_mult" / unit_container.gd:58 key 错位:"hp_mult" [var hp_mult: floa… | 验红 | 5ms | 1 |
| table_structure_guard | smoke_difficulty_table | 47 | "atk 修饰 ×2.0(%d→%d)" % [int(d0.atk), int(g2.atk)] | inject | unit_container.gd:58 key 错位:"atk_mult" / unit_container.gd:59 key 错位:"atk_mult" [var atk_mult: f… | 验红 | 5ms | 0 |
| table_structure_guard | smoke_difficulty_table | 48 | "越域修饰 spawn 受理" | inject | unit_container.gd:116 bool true→false / unit_container.gd:117 bool true→false [return true → ret… | 验红 | 5ms | 2 |
| table_structure_guard | smoke_difficulty_table | 49 | "越域乘数 clamp 至 10(%d→%d)" % [int(d0.hp), int(enemy_system_c.all_enemies()[0].hp)] | inject | unit_container.gd:57 key 错位:"hp_mult" / unit_container.gd:58 key 错位:"hp_mult" [var hp_mult: floa… | 验红 | 5ms | 1 |
| table_structure_guard | smoke_nine_types_gp_stream | 50 | "格面资源十型封闭(第九型 gp 九型批 2026-09-20 / 第十型 treasure_item 秘宝内容批 2026-09-29——词表 v1.2)" | | | 异常 | — | — |
| table_structure_guard | smoke_nine_types_gp_stream | 51 | "奖励字典 18 行(九型×正负 18 格——aux 挖格投放四行已退役 2026-10-01 用户拍板甲:战斗/挖格不掉道具,秘宝唯一来路=三选一共享池〔节点/通关〕;清图槽独立批尾不计)" | table | reward.rows:-1 table 复插行 / 表复插行 reward.rows[0] → 表首 | 验红 | 5ms | 0 |
| table_structure_guard | smoke_nine_types_gp_stream | 52 | "危险格负向直伤 %s power=%d(卡⑧投值)" % [rid, int(mine_power[rid])] | table | reward.rows[7].power:-1 table_set / 表行改写 reward.rows[7].power: 6 → 5 | 验红 | 5ms | 0 |
| table_structure_guard | smoke_nine_types_gp_stream | 53 | "正向 cell 行权重 ≥1 全在池 / 负向行权重恒 0 不入池(正向合计 %d——配置推导)" % pos_weight | table | reward.rows[0].weight:-1 table_set / 表行改写 reward.rows[0].weight: 14 → 0 | 验红 | 5ms | 0 |
| table_structure_guard | smoke_nine_types_gp_stream | 54 | "gp 型两行(GP 袋 gp_gain 正向 weight 2 / 破财 gp_loss 负向 weight 0 不入池)" | table | reward.rows:-1 table 删行 / 表删行 reward.rows[18] | 验红 | 5ms | 1 |
| table_structure_guard | smoke_nine_types_gp_stream | 55 | "derive_at 纯函数不推计数器(统一流全局计数器恒 0——并流后纯函数语义不变,gp_drop 专用向量随分盐作废、统一向量中央断言)" | inject | rng_service.gd:103 return 取反 / rng_service.gd:104 return 取反 [return _counter → return not (_coun… | 验红 | 5ms | 0 |
| table_structure_guard | smoke_adventure_table | 0 | "db.adventure 已加载(adventure.tres 入 manifest)" | | | 异常 | — | — |
| table_structure_guard | smoke_adventure_table | 1 | "AdventureTable.validate() 零违例(全键域检+档位配额结构锚在案)" | table | adventure.clear_heal_pct:-1 table_set / 表改写 adventure.clear_heal_pct: 20 → 200 | 验红 | 117ms | 1 |
| table_structure_guard | smoke_adventure_table | 2 | "clear_heal_pct ∈ [0,100](%d——通关部分回血比例)" % int(_db.adventure.clear_heal_pct) | table | adventure.clear_heal_pct:-1 table_set / 表改写 adventure.clear_heal_pct: 20 → 200 | 验红 | 125ms | 1 |
| table_structure_guard | smoke_adventure_table | 3 | "档位配额结构锚(四级 70/25/7/0——迭代 31 卡② 净删两件投放):%s = %d 张" % [tier, int(_db.adventure.TIER_QUOTAS[tier])… | table | adventure.curio_quota_attack_common:-1 table_set / 表改写 adventure.curio_quota_attack_common: 18 →… | 验红 | 125ms | 2 |
| table_structure_guard | smoke_adventure_table | 4 | "令牌池总量 = 102 张(四级 普通 70/优秀 25/稀有 7/传说 0——迭代 31 卡② 净删两件投放 104→102)〔凭证裁削批 V5〕" | table | adventure.curio_quota_attack_common:-1 table_set / 表改写 adventure.curio_quota_attack_common: 18 →… | 验红 | 124ms | 2 |
| table_structure_guard | smoke_adventure_table | 5 | "数值对(四级换代承袭):小GP 20/大GP 50 · 小增伤 +1/大 +2 · 小生命 +5/大 +10 · 小护甲 +1/大 +2" | table | adventure.curio_value_attack_common:-1 table_set / 表改写 adventure.curio_value_attack_common: 1 → … | 验红 | 126ms | 0 |
| table_structure_guard | smoke_adventure_table | 6 | "恢复件 +6 当即回血(优秀档承袭)· AP +1(稀有档承袭)〔专长批 V14 换代〕" | | | 异常 | — | — |
| table_structure_guard | smoke_adventure_table | 7 | "cap 占位在域(案②提前退役——99=实质无上限,实数归调参批)〔专长批 V8 形态面〕" | | | 异常 | — | — |
| table_structure_guard | smoke_adventure_table | 8 | "同调位读数源=树同调位节点点亮数(迭代 21 T4 槽位收编——null 档求值 0;谓词表已退役)〔天赋树批 V14〕" | | | 异常 | — | — |
| table_structure_guard | smoke_adventure_table | 9 | "新目录 12 件全量落表(旧 12 行删除;装配词表=活行 12 件零重复)〔秘宝批 V1〕" | table | treasure_items.rows:-1 table 复插行 / 表复插行 treasure_items.rows[0] → 表首 | 验红 | 119ms | 2 |
| table_structure_guard | smoke_adventure_table | 10 | "命名锁定:12 件 item_id 与 display_name 逐件=正式名(C2 代号即名)〔秘宝批 V2 初筛〕" | table | treasure_items.rows[0].item_id:-1 table 删键 / 表行删键 treasure_items.rows[0].item_id | 验红 | 125ms | 1 |
| table_structure_guard | smoke_adventure_table | 11 | "逐行解锁方式与契约 8 分配一致(钥石 8 件带价格、条件 4 件带谓词行互斥)+三档 7/3/2〔秘宝批 V1〕" | table | treasure_items.rows[0].item_id:-1 table 删键 / 表行删键 treasure_items.rows[0].item_id | 验红 | 117ms | 1 |
| table_structure_guard | smoke_adventure_table | 12 | "装配→战斗贯通:enc.attunement=[morning_order] 起战 attune_has=true(注入面+只读出口)〔秘宝批换主语〕" | inject | battle_orchestrator.gd:198 return 取反 / battle_orchestrator.gd:199 return 取反 [return _attune_has(… | 验红 | 320ms | 1 |
| table_structure_guard | smoke_adventure_table | 13 | "空装配基线:不带 attunement 起战 attune_has=false(off 态零扰动)〔秘宝批换主语〕" | inject | battle_orchestrator.gd:198 return 取反 / battle_orchestrator.gd:199 return 取反 [return _attune_has(… | 验红 | 309ms | 1 |
| table_structure_guard | smoke_adventure_table | 14 | "新 12 行效果词零棋盘改写类(危险格/安全格分布同池冻结——真视/开格动作外无写面)〔秘宝批 V9〕" | | | 异常 | — | — |
| table_structure_guard | smoke_adventure_table | 15 | "效果三方向覆盖在册(信息/行动资源/操作能力=plan·抉择刷新——12 件目录结构对齐设计权威)〔秘宝批 V10 覆盖半〕" | | | 异常 | — | — |
| table_structure_guard | smoke_adventure_table | 16 | "装配面空目录合法态:白板档 12 件全未解锁且 mode 分派正确(空目录=合法可玩态)〔秘宝批 V18〕" | table | treasure_items.rows:-1 table 复插行 / 表复插行 treasure_items.rows[0] → 表首 | 验红 | 119ms | 2 |
| table_structure_guard | smoke_meta | 56 | "MetaTable 非空 + validate 零错(三行族词表封闭/唯一/连续/域界/first≥repeat/ratio 全过)" | table | meta.boss_keystone[0].repeat_kill:-1 table 删键 / 表行删键 meta.boss_keystone[0].repeat_kill | 验红 | 5ms | 0 |
| table_structure_guard | smoke_meta | 57 | "MetaTable 行族规模 3/3/2(Boss/换算恰三/Y 升级两档——forge_products 迭代 21 退役、workshop_items 迭代 22 退役)" | table | meta.boss_keystone:-1 table 复插行 / 表复插行 meta.boss_keystone[0] → 表首 | 验红 | 5ms | 1 |
| table_structure_guard | smoke_meta | 58 | "conversion attr 词表封闭恰全(三属性轴——meta_* 访问器可达)" | table | meta.conversion[0].attr:-1 table 删键 / 表行删键 meta.conversion[0].attr | 验红 | 5ms | 1 |
| table_structure_guard | smoke_meta | 59 | "boss_keystone L1–L3 行连续可达且 first_kill ≥ repeat_kill 全行(首杀全量/重复小额)" | table | meta.boss_keystone[0].level_index:-1 table 删键 / 表行删键 meta.boss_keystone[0].level_index | 验红 | 5ms | 1 |
| board_gen_reward | smoke_layout_cell_formula | 0 | "L%d %dx%d 板网格高 %d ≤ 可用高 %d − 预留 24(格边 %dpx——下缘不贴边不裁切)" % [L, b.width, b.height, grid_h, int(ava… | inject | board_view.gd:226 op ==→!= / board_view.gd:227 op ==→!= [if _board == null or _board.width == 0:… | 验红 | 214ms | 0 |
| board_gen_reward | smoke_layout_cell_formula | 1 | "L%d 格边在 26–46 档内(%dpx)" % [L, int(cell)] | inject | board_view.gd:233 num 46→23(折半) / board_view.gd:234 num 46→23(折半) [_cell = float(clampi(cell, 26… | 验红 | 294ms | 0 |
| board_gen_reward | smoke_reward_identity_unit | 2 | "roll_board 受理" | inject | reward_economy.gd:41 if 取反 / reward_economy.gd:42 if 取反 [if eligible.is_empty(): → if not (eligi… | 验红 | 101ms | 0 |
| board_gen_reward | smoke_reward_identity_unit | 3 | "重复 roll_board 被拒(恰一次——double-roll 守卫)" | inject | reward_economy.gd:40 bool false→true / reward_economy.gd:41 bool false→true [return false → retu… | 验红 | 106ms | 0 |
| board_gen_reward | smoke_reward_identity_unit | 4 | "query_identity 投影 = {reward_id, cell_type, tier} 且五型封闭(样本 %s)" % str(types_seen.keys()) | inject | reward_economy.gd:112 key 错位:"reward_id" / reward_economy.gd:113 key 错位:"reward_id" [return {"re… | 验红 | 101ms | 0 |
| board_gen_reward | smoke_reward_identity_unit | 5 | "清单外坐标查询 = null(封锁格无身份同口径)" | inject | reward_economy.gd:110 return 取反 / reward_economy.gd:111 return 取反 [return null → return not (nul… | 验红 | 101ms | 0 |
| board_gen_reward | smoke_reward_identity_unit | 6 | "同格重复 grant 幂等(同一快照——受理恰一次)" | | | 异常 | — | — |
| board_gen_reward | smoke_reward_identity_unit | 7 | "清单外 grant 拒绝" | inject | reward_economy.gd:136 return 取反 / reward_economy.gd:137 return 取反 [return null → return not (nul… | 验红 | 100ms | 0 |
| board_gen_reward | smoke_reward_identity_unit | 8 | "清图批尾身份 = clear_blast(clear_default 唯一活行)" | inject | reward_economy.gd:342 op ==→!= / reward_economy.gd:343 op ==→!= [if row.reward_id == rid: → if r… | 验红 | 100ms | 1 |
| board_gen_reward | smoke_reward_identity_unit | 9 | "清图奖励 power ∈ 表区间 [%d, %d](roll 值 %d——配置推导)" % [cb_range[0], cb_range[1], int(cs1.power)] | inject | reward_economy.gd:342 op ==→!= / reward_economy.gd:343 op ==→!= [if row.reward_id == rid: → if r… | 验红 | 101ms | 1 |
| board_gen_reward | smoke_reward_identity_unit | 10 | "grant_clear 同批次重调幂等" | | | 异常 | — | — |
| board_gen_reward | smoke_reward_identity_unit | 11 | "映射退役后 grant 拒绝(生命周期闭合)" | inject | reward_economy.gd:133 return 取反 / reward_economy.gd:134 return 取反 [return null → return not (nul… | 验红 | 100ms | 0 |
| board_gen_reward | smoke_board_generator | 12 | "BoardGen 行解析 (%d,0)" % L | | | 异常 | — | — |
| board_gen_reward | smoke_board_generator | 13 | "R1 ceil 解析 (2,3)→(2,4)(向上取整偏难)" | | | 异常 | — | — |
| board_gen_reward | smoke_board_generator | 14 | "L%d 生成产物非空" % L | inject | board_generator.gd:206 key 错位:"inferability" / board_generator.gd:207 key 错位:"inferability" ["in… | 验红 | 85ms | 0 |
| board_gen_reward | smoke_board_generator | 15 | "L%d clearable(安全格 8 邻连通)" % L | inject | board_generator.gd:268 if 去not / board_generator.gd:269 if 去not [if not safe_set.has(c): → if sa… | 验红 | 1013ms | 4 |
| board_gen_reward | smoke_board_generator | 16 | "L%d legality 全过" % L | inject | board_generator.gd:282 return 取反 / board_generator.gd:283 return 取反 [return reasons.is_empty() →… | 验红 | 509ms | 2 |
| board_gen_reward | smoke_board_generator | 17 | "L%d inferability %.3f %s target %.2f(accept_mode=%s)" % [L, inf, "≥" if inf >= float(row.infera… | | | 异常 | — | — |
| board_gen_reward | smoke_board_generator | 18 | "L%d 生成器产物零变体格(变异布置归战斗初始化第 4 步——生成器管线不布置,2026-09-19 用户拍板棋盘设置顺序)" % L | | | 异常 | — | — |
| board_gen_reward | smoke_board_generator | 19 | "L%d 指标集输出降级标记与目标雷数(三分出口表)" % L | inject | board_generator.gd:341 key 错位:"target_mines" / board_generator.gd:342 key 错位:"target_mines" ["ta… | 验红 | 67ms | 0 |
| board_gen_reward | smoke_board_generator | 20 | "L%d 降级产物标记 = (accept_mode==accept_best)(%s→%s)" % [L, mode, str(b1.degraded_product)] | inject | board_generator.gd:353 op ==→!= / board_generator.gd:354 op ==→!= [b.degraded_product = accept_m… | 验红 | 1237ms | 0 |
| board_gen_reward | smoke_board_generator | 21 | "L%d 非法产物标记 = (accept_mode==accept_worst)(%s→%s)" % [L, mode, str(b1.illegal_product)] | inject | board_generator.gd:352 op ==→!= / board_generator.gd:353 op ==→!= [b.illegal_product = accept_mo… | 验红 | 1141ms | 2 |
| board_gen_reward | smoke_board_generator | 22 | "L%d 降级/非法产物标记互斥(两出口不可同真)" % L | | | 异常 | — | — |
| board_gen_reward | smoke_board_generator | 23 | "L%d 产物断言:实际雷数 %d ≤ 目标雷数 N=%d(先布后删只减不增)" % [L, int(b1.metrics.actual_mines), int(b1.metrics.targ… | | | 异常 | — | — |
| board_gen_reward | smoke_board_generator | 24 | "L%d attempts_used ∈ [1,%d]" % [L, int(row.max_gen_attempts)] | inject | board_generator.gd:343 key 错位:"attempts_used" / board_generator.gd:344 key 错位:"attempts_used" ["… | 验红 | 1149ms | 0 |
| board_gen_reward | smoke_board_generator | 25 | "L%d 非法产物标记 = false" % L | inject | board_generator.gd:282 return 置假 / board_generator.gd:283 return 置假 [return reasons.is_empty() →… | 验红 | 1123ms | 2 |
| board_gen_reward | smoke_board_generator | 26 | "L%d 同种子两次生成逐格相同" % L | | | 异常 | — | — |
| board_gen_reward | smoke_board_generator | 27 | "L%d 异种子产物不同(退化护栏)" % L | | | 异常 | — | — |
| board_gen_reward | smoke_board_generator | 28 | "L%d r0_max_cap 正式行投值在位(%d ≥ 出生保障面 9——板面 30%% 锚)" % [L, cap_val] | | | 异常 | — | — |
| board_gen_reward | smoke_board_generator | 29 | "L%d 结构断言 R₀=%d ≤ cap=%d(R₀≤cap 重掷闸承载)" % [L, int(b1.metrics.r0_count), cap_val] | inject | board_generator.gd:282 return 取反 / board_generator.gd:283 return 取反 [return reasons.is_empty() →… | 验红 | 511ms | 2 |
| board_gen_reward | smoke_board_generator | 30 | "L%d r0_cap_rejected 尝试事实指标在位(本图 %d 次触发)" % [L, int(b1.metrics.get("r0_cap_rejected", -1))] | inject | board_generator.gd:350 key 错位:"r0_cap_rejected" / board_generator.gd:351 key 错位:"r0_cap_rejected… | 验红 | 1278ms | 0 |
| board_gen_reward | smoke_board_generator | 31 | "L%d ring_removed=%d ≤ 剃雷域候选危险格数((3+2r)²−9——全域剃雷退役)" % [L, int(b1.metrics.get("ring_removed", 99… | inject | board_generator.gd:347 key 错位:"ring_removed" / board_generator.gd:348 key 错位:"ring_removed" ["ri… | 验红 | 1220ms | 0 |
| board_gen_reward | smoke_board_generator | 32 | "L1 cap 压线 9:出图 R₀=%d %s(三分出口诚实降级——超阈即 accept_worst 非法标记,玩家永不卡死)" % [int(b9.metrics.r0_count), "… | inject | board_generator.gd:282 return 取反 / board_generator.gd:283 return 取反 [return reasons.is_empty() →… | 验红 | 490ms | 2 |
| board_gen_reward | smoke_board_generator | 33 | "L1 cap 压线出图指标集完整(accept_mode=%s / 闸触发 %d 次)" % [String(b9.metrics.get("accept_mode", "")), int(… | inject | board_generator.gd:344 key 错位:"accept_mode" / board_generator.gd:345 key 错位:"accept_mode" ["acce… | 验红 | 1292ms | 2 |
| board_gen_reward | smoke_power_interval | 34 | "区间行配对齐全且 power = floor 中点(配置推导 %d 行:%s)" % [interval_rows.size(), str(interval_rows.keys())] | table | reward.rows[3].power:-1 table_set / 表行改写 reward.rows[3].power: 2 → 1 | 验红 | 8ms | 0 |
| board_gen_reward | smoke_power_interval | 35 | "24 板全区间行快照 power ∈ [power_min, power_max](含清图槽)" | table | reward.rows[3].power_max:-1 table_set / 表行改写 reward.rows[3].power_max: 2 → 1 | 验红 | 8ms | 1 |
| board_gen_reward | smoke_power_interval | 36 | "定值行快照 power 恒等表值——区间批零外溢" | inject | reward_economy.gd:125 num 1→0 / reward_economy.gd:126 num 1→0 [return _snapshot(_row_by_id(rid),… | 验红 | 8ms | 0 |
| board_gen_reward | smoke_power_interval | 37 | "区间行上下阶实际流动(≥2 样本的区间行须 ≥2 个不同 roll 值)" | | | 异常 | — | — |
| board_gen_reward | smoke_power_interval | 38 | "定值行(lo=hi)roll 恒等表值" | | | 异常 | — | — |
| roll_multiplier | smoke_enemy_attack_type | 0 | "敌方 %d 行 attack_type 全填 normal(迭代 24 N3 轴开列,越轨行%s)" % [ _db.enemy.rows.size(), at_detail if at_d… | table | enemy.rows[0].attack_type:-1 table_set / 表行改写 enemy.rows[0].attack_type: normal → normal_x | 验红 | 185ms | 0 |
| roll_multiplier | smoke_enemy_attack_type | 1 | "敌方单位快照 attack_type 非空(spawn 注入面——迭代 24 N3)" | | | 异常 | — | — |
| roll_multiplier | smoke_enemy_attack_type | 2 | "克制矩阵命中证词:同种子三甲型实测 heavy %d == light %d − 1 · ward %d == light %d + 1(迭代 24 N3 全局平移 −1/0/+1)" % … | table | enemy.rows[5].level_index:-1 table_set / 表行改写 enemy.rows[5].level_index: 2 → 3 | 验红 | 1673ms | 0 |
| roll_multiplier | smoke_enemy_attack_type | 3 | "敌方伤害技单据载荷 attack_type 非空(= %s——第 9 参传值面,迭代 24 N3)" % payload_at | table | enemy.rows[13].unit_count:-1 table_set / 表行改写 enemy.rows[13].unit_count: 1 → 0 | 验红 | 5850ms | 1 |
| roll_multiplier | smoke_armor_roll | 4 | "查档 AC%d 全掷点域分布(100/75/50/25 = %s)" % [cfg.ac, str([dist[100], dist[75], dist[50], dist[25]])] | table | settlement.roll_tier_bounds:-1 table 删行 / 表删行 settlement.roll_tier_bounds[0] | 验红 | 1135ms | 1 |
| roll_multiplier | smoke_armor_roll | 5 | "AC10 EV 锚:期望系数合计 %d/2000 = 期望减免 21.25%%(nat20 恒全额含内)" % ev_sum | table | settlement.roll_tier_bounds:-1 table 删行 / 表删行 settlement.roll_tier_bounds[0] | 验红 | 1131ms | 1 |
| roll_multiplier | smoke_armor_roll | 6 | "nat20 种子可达(seed=%d——主角先手首掷命中 %d)" % [crit_seed, int(_db.settlement.crit_roll)] | table | settlement.roll_faces:-1 table_set / 表改写 settlement.roll_faces: 20 → 19 | 验红 | 50384ms | 2 |
| roll_multiplier | smoke_armor_roll | 7 | "nat20 → 行留档 roll=20 + crit 标记(主角先手普通分量)" | table | settlement.roll_faces:-1 table_set / 表改写 settlement.roll_faces: 20 → 19 | 验红 | 50180ms | 2 |
| roll_multiplier | smoke_armor_roll | 8 | "nat20 → 普通分量 ×2(%d = 2×%d,暴击倍率只乘该分量,穿刺/魔法不吃)" % [int(crit_rows.get("normal", -1)), int(_db.hero… | table | settlement.roll_faces:-1 table_set / 表改写 settlement.roll_faces: 20 → 19 | 验红 | 50107ms | 2 |
| roll_multiplier | smoke_armor_roll | 9 | "削弱重复授予 → 层数保持 1、剩余结算数重置 2(续时语义——结构性消饱和)" | | | 异常 | — | — |
| roll_multiplier | smoke_armor_roll | 10 | "削弱单层池读数 = 行值(enemy_output 池单层贡献——×2 叠满触底 ×0 不可达,免伤轮根治)" | table | settlement.effect_pool_map.weaken:-1 table_set / 表行改写 settlement.effect_pool_map.weaken: enemy_o… | 验红 | 1139ms | 0 |
| roll_multiplier | smoke_armor_roll | 11 | "AC11 攻击拍逐拍判定(×%d 拍全部落档域 {100,75,50,25})" % tiers.size() | table | settlement.roll_faces:-1 table_set / 表改写 settlement.roll_faces: 20 → 0 | 验红 | 51284ms | 4 |
| roll_multiplier | smoke_armor_roll | 12 | "折扣档实际流动(AC11 实测档位 %s——≥2 档可见)" % str(tier_distinct.keys()) | table | settlement.roll_faces:-1 table_set / 表改写 settlement.roll_faces: 20 → 0 | 验红 | 51319ms | 4 |
| roll_multiplier | smoke_armor_roll | 13 | "全战斗流零 v=0 伤害行(伤害下限全局生效)" | | | 异常 | — | — |
| roll_multiplier | smoke_armor_roll | 14 | "hero_attack/enemy_action 伤害行掷点读数+档位随行留档(P2 归因)" | | | 异常 | — | — |
| roll_multiplier | smoke_armor_roll | 15 | "棋盘来源伤害行零掷点字段(爆裂/直伤/清障炸药/奖励即时必中全额)" | | | 异常 | — | — |
| roll_multiplier | smoke_armor_roll | 16 | "棋盘克制直伤贴底 → 下限 1(点数 2 vs 不利修正 −1)" | table | settlement.damage_floor:-1 table_set / 表改写 settlement.damage_floor: 1 → 2 | 验红 | 1152ms | 1 |
| roll_multiplier | smoke_armor_roll | 17 | "克制修正触底穿透 → 下限 1(点数 1 vs 修正 −3)" | table | settlement.damage_floor:-1 table_set / 表改写 settlement.damage_floor: 1 → 2 | 验红 | 1123ms | 1 |
| roll_multiplier | smoke_armor_roll | 18 | "空桶不吃修正恒 0(零点数分量无伤害可落地——边界不变不受下限影响)" | | | 异常 | — | — |
| roll_multiplier | smoke_pool_wiring | 19 | "单分量基线在位(normal 分量 = 落地值、基线 %d——充能退役批)" % base_v | | | 异常 | — | — |
| roll_multiplier | smoke_pool_wiring | 20 | "增伤经乘区加成合计(%d → %d = ×1.5——attack_up 直乘旁路退役,覆盖面=攻击合计)" % [base_v, int(up_row.get("value", -1))] | table | status_effects.rows[0].effect_id:-1 table 删键 / 表行删键 status_effects.rows[0].effect_id | 验红 | 194ms | 0 |
| roll_multiplier | smoke_pool_wiring | 21 | "乏力经乘区削减合计(%d → %d = ×0.75)" % [base_v, int(sapped_row.get("value", -1))] | table | status_effects.rows[5].effect_id:-1 table 删键 / 表行删键 status_effects.rows[5].effect_id | 验红 | 194ms | 0 |
| roll_multiplier | smoke_pool_wiring | 22 | "硬化折半方向(%d → %d = ×0.5 且 < 基线——敌方防御池负值减伤,翻倍 bug 退役)" % [base_v, int(fortify_row.get("value", -1)… | table | status_effects.rows[7].effect_id:-1 table 删键 / 表行删键 status_effects.rows[7].effect_id | 验红 | 203ms | 0 |
| roll_multiplier | smoke_damage_types | 23 | "goblin 护甲 = light(轻甲——教学位)" | table | enemy.rows[0].unit_count:-1 table_set / 表行改写 enemy.rows[0].unit_count: 1 → 0 | 验红 | 412ms | 0 |
| roll_multiplier | smoke_damage_types | 24 | "bugbear 护甲 = light(护甲类型重配批 2026-09-18:关卡 1 全轻甲)" | table | enemy.rows[2].unit_count:-1 table_set / 表行改写 enemy.rows[2].unit_count: 1 → 0 | 验红 | 400ms | 0 |
| roll_multiplier | smoke_damage_types | 25 | "ogre_chief 护甲 = light(Boss 行集——护甲类型重配批 2026-09-18:关卡 1 全轻甲)" | table | enemy.rows[3].armor_type:-1 table_set / 表行改写 enemy.rows[3].armor_type: light → light_x | 验红 | 397ms | 0 |
| roll_multiplier | smoke_damage_types | 26 | "普通行:轻甲 0 / 魔法护甲 +1 / 重甲 −1(加法核)" | | | 异常 | — | — |
| roll_multiplier | smoke_damage_types | 27 | "穿刺行:轻甲 −1 / 魔法护甲 +1 / 重甲 0 中性(用户拍板)" | | | 异常 | — | — |
| roll_multiplier | smoke_damage_types | 28 | "魔法行:轻甲 0 / 魔法护甲 −1 / 重甲 +1(加法核)" | | | 异常 | — | — |
| roll_multiplier | smoke_damage_types | 29 | "标错封印产生 seal_miss 归因行(×%d)" % miss_rows | table | enemy.rows[4].level_index:-1 table 删键 / 表行删键 enemy.rows[4].level_index | 验红 | 68ms | 2 |
| roll_multiplier | smoke_damage_types | 30 | "标错封印兑付该安全格资源的负向字典行(×%d——attack/defense/weaken 效果行或 heal 直伤行)" % negative_rows | table | enemy.rows[4].level_index:-1 table 删键 / 表行删键 enemy.rows[4].level_index | 验红 | 72ms | 2 |
| roll_multiplier | smoke_damage_types | 31 | "封印命中兑付该危险格资源的正向字典行(×%d——充能退役后无魔法分量,正向=格面资源)" % hit_rows | table | enemy.rows[4].level_index:-1 table 删键 / 表行删键 enemy.rows[4].level_index | 验红 | 67ms | 2 |
| roll_multiplier | smoke_damage_types | 32 | "标错惩罚轮回放断言:同种子步骤流逐行相同(类型化负向兑付确定性)" | | | 异常 | — | — |
| roll_multiplier | smoke_attack_chain_fresh_pool | 33 | "fresh 池回归战斗启动(Boss 行)" | inject | battle_orchestrator.gd:380 asgn 置null / battle_orchestrator.gd:381 asgn 置null [_ledger = ledger_… | 验红 | 249ms | 1 |
| roll_multiplier | smoke_attack_chain_fresh_pool | 34 | "非施放攻击拍采样充足(%d 拍——增益施放行零兑付为设计语义不计)" % ticks.size() | inject | battle_orchestrator.gd:304 key 错位:"[战斗结算] begin_battle " / battle_orchestrator.gd:305 key 错位:"[战… | 验红 | 324ms | 0 |
| roll_multiplier | smoke_attack_chain_fresh_pool | 35 | "fresh 池非蓄力拍 d_tick = max(1, 掷点档位基线)(Boss 攻击链健康——Bug#2 回归断言)" | | | 异常 | — | — |
| seal_polarity | smoke_seal_ap_exhausted | 0 | "回归战斗启动(Boss 行——D0 20HP 级会被盲挖粉尘秒杀,无法承载封印回归场景)" | state | battle_orchestrator.gd:43 num 0→1 / [状态·初值] battle_orchestrator.gd:44 num 0→1 [var round_index :… | 验红 | 113ms | 4 |
| seal_polarity | smoke_seal_ap_exhausted | 1 | "%d 次挖掘后恰剩 1 AP(phase=%s ap=%d)" % [ap_total - 1, str(settlement.action_economy.phase), settleme… | state | battle_orchestrator.gd:43 num 0→1 / [状态·初值] battle_orchestrator.gd:44 num 0→1 [var round_index :… | 验红 | 131ms | 4 |
| seal_polarity | smoke_seal_ap_exhausted | 2 | "仍存在可挖目标(封印目标来源)" | state | battle_orchestrator.gd:43 num 0→1 / [状态·初值] battle_orchestrator.gd:44 num 0→1 [var round_index :… | 验红 | 114ms | 4 |
| seal_polarity | smoke_seal_ap_exhausted | 3 | "末点 AP 封印被受理" | state | battle_orchestrator.gd:43 num 0→1 / [状态·初值] battle_orchestrator.gd:44 num 0→1 [var round_index :… | 验红 | 113ms | 4 |
| seal_polarity | smoke_seal_ap_exhausted | 4 | "封印动作耗尽 AP 触发轮末结算并进入第 2 轮(rounds=%s)" % str(rounds) | state | battle_orchestrator.gd:20 key 错位:"decay" / [状态·初值] battle_orchestrator.gd:21 key 错位:"decay" [con… | 验红 | 362ms | 0 |
## 二、异常清单(打不红的条+原因)
- system_test:smoke_scene_split #1「menu.start_overlay != null and menu.lvbtn != null and menu.resumebtn != null and menu.campbtn !=…」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形
- system_test:smoke_scene_split #2「menu.start_overlay.visible」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形;断言表达式读 schema 常量(大写成员,宿主 preload 编译期绑定原件)——…
- system_test:smoke_scene_split #3「menu.mouse_filter == Control.MOUSE_FILTER_IGNORE」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形;断言表达式读 schema 常量(大写成员,宿主 preload 编译期绑定原件)——…
- system_test:smoke_scene_split #4「camp.camp_overlay != null」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形
- system_test:smoke_scene_split #5「not camp.camp_overlay.visible」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形
- system_test:smoke_scene_split #6「adv.battle_root != null and adv.stage != null and adv.board_zone != null and adv.board_view != n…」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形
- system_test:smoke_boot_chain #12「db.is_ready and db.hero != null」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形
- system_test:smoke_boot_chain #13「AudioServer.is_bus_mute(master) == (ss.volume_linear <= 0.001) and is_equal_approx(AudioServer.g…」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形
- system_test:smoke_boot_chain #14「ui_kit.motion_scale == (0.5 if ss.reduced_motion else 1.0)」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形
- system_test:smoke_boot_chain #16「not sm.has_breakpoint() and not main._resume_btn.visible」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形
- system_test:smoke_boot_chain #18「sm.keystone_balance() == 0 and not sm.has_breakpoint() and sm.adventure_total() == 0」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形
- system_test:smoke_boot_chain #20「not (FileAccess.file_exists("user://save.json") and not FileAccess.file_exists(sm.slot_path(1)))」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形;断言表达式含磁盘读取(FileAccess/DirAccess)——内存内改坏不可达,…
- system_test:smoke_boot_chain #21「not sm.has_breakpoint() and not main._resume_btn.visible」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形
- system_test:smoke_boot_chain #23「not main._resume_btn.visible」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形
- system_test:smoke_boot_chain #25「not main.debug_overlay.visible」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形
- system_test:smoke_boot_chain #—「」——跑后基线未复绿——存在跨试跑状态残留,须人工排查
- system_test:smoke_run_lifecycle #28「main.run_scope.hints_shown.is_empty()」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形
- system_test:smoke_run_lifecycle #29「main.run_scope.adventure.abandon()」——复验未红(改坏候选 1 个均不复现——疑似非确定断言)
- system_test:smoke_run_lifecycle #35「not sm.has_breakpoint()」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形
- system_test:smoke_lifecycle_matrix #39「main.run_scope.settlement == null or not main.run_scope.settlement.is_battle_live()」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形
- system_test:smoke_lifecycle_matrix #40「main.run_scope.adventure.abandon()」——复验未红(改坏候选 1 个均不复现——疑似非确定断言)
- system_test:smoke_lifecycle_matrix #41「finished.done and main._end_overlay.is_open()」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形
- system_test:smoke_lifecycle_matrix #44「false」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形
- system_test:smoke_lifecycle_matrix #45「main.run_scope.adventure != null and main.run_scope.adventure != first_adv」——预算内未找到能翻红的改坏(试跑 600 轮,武器=inject, state)——死断言候选或改坏面缺形
- system_test:smoke_lifecycle_matrix #46「main.run_scope.adventure.abandon()」——复验未红(改坏候选 1 个均不复现——疑似非确定断言)
- table_structure_guard:smoke_ac_swap_guard #25「"hero_defense 池 percent 行叠满不可触池底(墙最深%s > clamp_lo %d——单效果归零结构性不可达)" % [ wall_detail if wall_deta…」——预算内未找到能翻红的改坏(试跑 1608 轮,武器=table, inject)——死断言候选或改坏面缺形
- table_structure_guard:smoke_enemy_bands #19「"hobgoblin 缺口登记随行(表文件注释在位——缺口不静默)〔fv25〕"」——预算内未找到能翻红的改坏(试跑 708 轮,武器=table, inject)——死断言候选或改坏面缺形;断言表达式含磁盘读取(FileAccess/DirAccess)——内存内改坏不可达,…
- table_structure_guard:smoke_nine_types_gp_stream #50「"格面资源十型封闭(第九型 gp 九型批 2026-09-20 / 第十型 treasure_item 秘宝内容批 2026-09-29——词表 v1.2)"」——预算内未找到能翻红的改坏(试跑 639 轮,武器=table, inject)——死断言候选或改坏面缺形;断言表达式读 schema 常量(大写成员,宿主 preload 编译期绑定原件)——…
- table_structure_guard:smoke_adventure_table #0「"db.adventure 已加载(adventure.tres 入 manifest)"」——预算内未找到能翻红的改坏(试跑 3000 轮,武器=table, inject)——死断言候选或改坏面缺形
- table_structure_guard:smoke_adventure_table #6「"恢复件 +6 当即回血(优秀档承袭)· AP +1(稀有档承袭)〔专长批 V14 换代〕"」——预算内未找到能翻红的改坏(试跑 3000 轮,武器=table, inject)——死断言候选或改坏面缺形
- table_structure_guard:smoke_adventure_table #7「"cap 占位在域(案②提前退役——99=实质无上限,实数归调参批)〔专长批 V8 形态面〕"」——预算内未找到能翻红的改坏(试跑 3000 轮,武器=table, inject)——死断言候选或改坏面缺形
- table_structure_guard:smoke_adventure_table #8「"同调位读数源=树同调位节点点亮数(迭代 21 T4 槽位收编——null 档求值 0;谓词表已退役)〔天赋树批 V14〕"」——预算内未找到能翻红的改坏(试跑 3000 轮,武器=table, inject)——死断言候选或改坏面缺形
- table_structure_guard:smoke_adventure_table #14「"新 12 行效果词零棋盘改写类(危险格/安全格分布同池冻结——真视/开格动作外无写面)〔秘宝批 V9〕"」——预算内未找到能翻红的改坏(试跑 3000 轮,武器=table, inject)——死断言候选或改坏面缺形
- table_structure_guard:smoke_adventure_table #15「"效果三方向覆盖在册(信息/行动资源/操作能力=plan·抉择刷新——12 件目录结构对齐设计权威)〔秘宝批 V10 覆盖半〕"」——预算内未找到能翻红的改坏(试跑 3000 轮,武器=table, inject)——死断言候选或改坏面缺形
- board_gen_reward:smoke_reward_identity_unit #6「"同格重复 grant 幂等(同一快照——受理恰一次)"」——预算内未找到能翻红的改坏(试跑 339 轮,武器=inject, state)——死断言候选或改坏面缺形;标签含幂等语义——确定性快照的内容等值在单行确定性变异下难破,疑似结构性打不红,须人工…
- board_gen_reward:smoke_reward_identity_unit #10「"grant_clear 同批次重调幂等"」——预算内未找到能翻红的改坏(试跑 339 轮,武器=inject, state)——死断言候选或改坏面缺形;标签含幂等语义——确定性快照的内容等值在单行确定性变异下难破,疑似结构性打不红,须人工…
- board_gen_reward:smoke_board_generator #12「"BoardGen 行解析 (%d,0)" % L」——预算内未找到能翻红的改坏(试跑 60 轮,武器=inject, table)——死断言候选或改坏面缺形
- board_gen_reward:smoke_board_generator #13「"R1 ceil 解析 (2,3)→(2,4)(向上取整偏难)"」——预算内未找到能翻红的改坏(试跑 60 轮,武器=inject, table)——死断言候选或改坏面缺形
- board_gen_reward:smoke_board_generator #17「"L%d inferability %.3f %s target %.2f(accept_mode=%s)" % [L, inf, "≥" if inf >= float(row.infera…」——复验未红(改坏候选 1 个均不复现——疑似非确定断言)
- board_gen_reward:smoke_board_generator #18「"L%d 生成器产物零变体格(变异布置归战斗初始化第 4 步——生成器管线不布置,2026-09-19 用户拍板棋盘设置顺序)" % L」——预算内未找到能翻红的改坏(试跑 60 轮,武器=inject, table)——死断言候选或改坏面缺形
- board_gen_reward:smoke_board_generator #22「"L%d 降级/非法产物标记互斥(两出口不可同真)" % L」——复验未红(改坏候选 1 个均不复现——疑似非确定断言)
- board_gen_reward:smoke_board_generator #23「"L%d 产物断言:实际雷数 %d ≤ 目标雷数 N=%d(先布后删只减不增)" % [L, int(b1.metrics.actual_mines), int(b1.metrics.targ…」——预算内未找到能翻红的改坏(试跑 60 轮,武器=inject, table)——死断言候选或改坏面缺形
- board_gen_reward:smoke_board_generator #26「"L%d 同种子两次生成逐格相同" % L」——预算内未找到能翻红的改坏(试跑 60 轮,武器=inject, table)——死断言候选或改坏面缺形
- board_gen_reward:smoke_board_generator #27「"L%d 异种子产物不同(退化护栏)" % L」——预算内未找到能翻红的改坏(试跑 60 轮,武器=inject, table)——死断言候选或改坏面缺形
- board_gen_reward:smoke_board_generator #28「"L%d r0_max_cap 正式行投值在位(%d ≥ 出生保障面 9——板面 30%% 锚)" % [L, cap_val]」——预算内未找到能翻红的改坏(试跑 60 轮,武器=inject, table)——死断言候选或改坏面缺形;断言表达式读 schema 常量(大写成员,宿主 preload 编译期绑定原件)——路…
- board_gen_reward:smoke_power_interval #37「"区间行上下阶实际流动(≥2 样本的区间行须 ≥2 个不同 roll 值)"」——预算内未找到能翻红的改坏(试跑 596 轮,武器=table, inject)——死断言候选或改坏面缺形
- board_gen_reward:smoke_power_interval #38「"定值行(lo=hi)roll 恒等表值"」——预算内未找到能翻红的改坏(试跑 596 轮,武器=table, inject)——死断言候选或改坏面缺形
- roll_multiplier:smoke_enemy_attack_type #1「"敌方单位快照 attack_type 非空(spawn 注入面——迭代 24 N3)"」——预算内未找到能翻红的改坏(试跑 600 轮,武器=table, inject)——死断言候选或改坏面缺形
- roll_multiplier:smoke_armor_roll #9「"削弱重复授予 → 层数保持 1、剩余结算数重置 2(续时语义——结构性消饱和)"」——预算内未找到能翻红的改坏(试跑 600 轮,武器=table, inject)——死断言候选或改坏面缺形
- roll_multiplier:smoke_armor_roll #13「"全战斗流零 v=0 伤害行(伤害下限全局生效)"」——预算内未找到能翻红的改坏(试跑 600 轮,武器=table, inject)——死断言候选或改坏面缺形
- roll_multiplier:smoke_armor_roll #14「"hero_attack/enemy_action 伤害行掷点读数+档位随行留档(P2 归因)"」——预算内未找到能翻红的改坏(试跑 600 轮,武器=table, inject)——死断言候选或改坏面缺形
- roll_multiplier:smoke_armor_roll #15「"棋盘来源伤害行零掷点字段(爆裂/直伤/清障炸药/奖励即时必中全额)"」——预算内未找到能翻红的改坏(试跑 600 轮,武器=table, inject)——死断言候选或改坏面缺形
- roll_multiplier:smoke_armor_roll #18「"空桶不吃修正恒 0(零点数分量无伤害可落地——边界不变不受下限影响)"」——预算内未找到能翻红的改坏(试跑 600 轮,武器=table, inject)——死断言候选或改坏面缺形
- roll_multiplier:smoke_pool_wiring #19「"单分量基线在位(normal 分量 = 落地值、基线 %d——充能退役批)" % base_v」——预算内未找到能翻红的改坏(试跑 500 轮,武器=table, inject)——死断言候选或改坏面缺形
- roll_multiplier:smoke_damage_types #26「"普通行:轻甲 0 / 魔法护甲 +1 / 重甲 −1(加法核)"」——预算内未找到能翻红的改坏(试跑 500 轮,武器=table, inject)——死断言候选或改坏面缺形
- roll_multiplier:smoke_damage_types #27「"穿刺行:轻甲 −1 / 魔法护甲 +1 / 重甲 0 中性(用户拍板)"」——预算内未找到能翻红的改坏(试跑 500 轮,武器=table, inject)——死断言候选或改坏面缺形
- roll_multiplier:smoke_damage_types #28「"魔法行:轻甲 0 / 魔法护甲 −1 / 重甲 +1(加法核)"」——预算内未找到能翻红的改坏(试跑 500 轮,武器=table, inject)——死断言候选或改坏面缺形
- roll_multiplier:smoke_damage_types #32「"标错惩罚轮回放断言:同种子步骤流逐行相同(类型化负向兑付确定性)"」——预算内未找到能翻红的改坏(试跑 500 轮,武器=table, inject)——死断言候选或改坏面缺形
- roll_multiplier:smoke_attack_chain_fresh_pool #35「"fresh 池非蓄力拍 d_tick = max(1, 掷点档位基线)(Boss 攻击链健康——Bug#2 回归断言)"」——预算内未找到能翻红的改坏(试跑 500 轮,武器=inject, state)——死断言候选或改坏面缺形
- system_test:smoke_boot_chain——跑后基线未复绿(存在跨试跑状态残留)
## 三、汇总
- system_test:split_tail——1 条(验红 1 / 异常 0)· 改坏试跑 1 轮 · 跑后基线复绿
- system_test:smoke_scene_split——11 条(验红 5 / 异常 6)· 改坏试跑 600 轮 · 跑后基线复绿
- system_test:smoke_boot_chain——15 条(验红 6 / 异常 9)· 改坏试跑 600 轮 · 跑后基线**未复绿**
- system_test:smoke_run_lifecycle——11 条(验红 8 / 异常 3)· 改坏试跑 600 轮 · 跑后基线复绿
- system_test:smoke_lifecycle_matrix——10 条(验红 4 / 异常 6)· 改坏试跑 600 轮 · 跑后基线复绿
- system_test:stage_faces——1 条(验红 1 / 异常 0)· 改坏试跑 10 轮 · 跑后基线复绿
- table_structure_guard:smoke_ac_swap_guard——12 条(验红 11 / 异常 1)· 改坏试跑 1608 轮 · 跑后基线复绿
- table_structure_guard:smoke_enemy_bands——4 条(验红 3 / 异常 1)· 改坏试跑 708 轮 · 跑后基线复绿
- table_structure_guard:smoke_variation_framework——9 条(验红 9 / 异常 0)· 改坏试跑 103 轮 · 跑后基线复绿
- table_structure_guard:smoke_difficulty_table——8 条(验红 8 / 异常 0)· 改坏试跑 131 轮 · 跑后基线复绿
- table_structure_guard:smoke_nine_types_gp_stream——6 条(验红 5 / 异常 1)· 改坏试跑 639 轮 · 跑后基线复绿
- table_structure_guard:smoke_adventure_table——17 条(验红 11 / 异常 6)· 改坏试跑 3000 轮 · 跑后基线复绿
- table_structure_guard:smoke_meta——4 条(验红 4 / 异常 0)· 改坏试跑 41 轮 · 跑后基线复绿
- board_gen_reward:smoke_layout_cell_formula——2 条(验红 2 / 异常 0)· 改坏试跑 14 轮 · 跑后基线复绿
- board_gen_reward:smoke_reward_identity_unit——10 条(验红 8 / 异常 2)· 改坏试跑 339 轮 · 跑后基线复绿
- board_gen_reward:smoke_board_generator——22 条(验红 13 / 异常 9)· 改坏试跑 60 轮 · 跑后基线复绿
- board_gen_reward:smoke_power_interval——5 条(验红 3 / 异常 2)· 改坏试跑 596 轮 · 跑后基线复绿
- roll_multiplier:smoke_enemy_attack_type——4 条(验红 3 / 异常 1)· 改坏试跑 600 轮 · 跑后基线复绿
- roll_multiplier:smoke_armor_roll——15 条(验红 10 / 异常 5)· 改坏试跑 600 轮 · 跑后基线复绿
- roll_multiplier:smoke_pool_wiring——4 条(验红 3 / 异常 1)· 改坏试跑 500 轮 · 跑后基线复绿
- roll_multiplier:smoke_damage_types——10 条(验红 6 / 异常 4)· 改坏试跑 500 轮 · 跑后基线复绿
- roll_multiplier:smoke_attack_chain_fresh_pool——3 条(验红 2 / 异常 1)· 改坏试跑 500 轮 · 跑后基线复绿
- seal_polarity:smoke_seal_ap_exhausted——5 条(验红 5 / 异常 0)· 改坏试跑 9 轮 · 跑后基线复绿
合计 189 条:验红 131 / 异常 58。
@@ -309,3 +309,123 @@
7. 波 2 改名后把旧目录名 `game/smoke` 加入熵增闸 RETIRED 清单(防复发)。
8. dump_battle/experiment_density 台账「一次性」件**只搬不删**。
9. 待办清单:570 条目删除归 batch-closure,波 2 不代删。
## 十一、波 1 搬家代理二施工记录(G9/G10/G11/G12 —— 2026-10-22 搬家代理二)
**作业形态=纯文本搬家**:零测试运行、零 git 提交、`game/smoke/smoke_all.gd` 与他人宿主零改动(我名下 22 个套函数与消费 helper 原文全部原地保留,删除归收尾批);manifest(run_all.gd)登记未做(归收尾装配统一登记)。六件宿主全部新建、均为未提交工作区文件——**以下各套状态=已搬运(待装配验证)**。
**G9 单元·元进度与锻造 → game/tests/unit/core/meta_progression_forge_test.gd(新建):六套 133 条全部迁入,已搬运(待装配验证)。**
- _meta_pricing_and_buy(4 条)/_meta_settle_and_workshop(42 条)/_meta_injection(10 条)/_smoke_forge_axes(33 条)/_smoke_tree(25 条)/_smoke_tree_rules(19 条)——各条已搬运(待装配验证)。
- 挂载终态=宿主 `_init` 自建 `_db`(load_tables(true) 失败不中止承 G2–G5 同款);套执行序沿源(_smoke_meta 薄壳序 pricing→settle_ws→injection→forge_axes,接 _run_core_smoke 尾段 tree→tree_rules)。
- 消费 helper(全部宿主自持私有副本):`_META_SMOKE_PATHS`(全 18 路径)+`_meta_smoke_cleanup/_meta_new_save/_fresh_rng`+`_run_battle/_auto_play_round`(**过渡副本**——去 G6 专属「收束相位回 Idle」断言,G4/G5 同款零断言纯驱动形态)+`AutoPlayer` 按现路径 `res://smoke/auto_player.gd` preload。全局 class_name 裸用:Attunement/EffectEngine/EffectContext(G3 先例)。
**G10 单元·存档域 → game/tests/unit/core/save_manager_domain_test.gd(新建):91 条迁入,已搬运(待装配验证)。**
- _smoke_save 60 条(原 70 条;⑨ 摘要集成块基点 :6807–6831 共 10 条按裁决三**转办集成**,宿主内留转办注记)——已搬运(待装配验证)。
- _smoke_save_slots 31 条——已搬运(待装配验证)。
- 消费 helper:`_fresh_rng`(自持副本)+`_db` 夹具(v2 续玩受理/收束链夹具消费);_smoke_save_slots 零 helper 自足。paths[8] `smoke_save_integration.json` 仅被转办块消费,名册照源保留(段首尾清场循环零害)。
**G11 单元·呈现投影与主题 → game/tests/unit/presentation/theme_projection_test.gd(新建):十套 120 条迁入,已搬运(待装配验证)。**
- _smoke_type_glow(24)/_smoke_battle_log_view(6)/_smoke_global_theme(22)/_smoke_button_five_states(23)/_smoke_texture_filter(9)/_smoke_contrast_inks(9)/_smoke_element_details(8)/_smoke_diag_button_retired(7)/_status_panel_projection(4)/_career_overlay_projection(8)——各条已搬运(待装配验证)。
- _smoke_playtest_logger 8 条按裁决七改判集成——**不随本宿主,转办**(见下方转办清单)。
- 挂载终态=`_db`+`_rng`(master 种子 42 沿源)双夹具;套执行序沿源(_run_core_smoke 序+_smoke_meta 薄壳尾序 status_panel→career_overlay)。
- 消费 helper(全部自持私有副本):`_theme_rel_luminance/_theme_contrast`+微光簇全件(`_GLOW_FIXTURE_TYPES` 常量+`_glow_type_overrides/_glow_blocked` 成员+`_glow_fixture_identity/_glow_unrevealed_neighbors/_glow_expected_set/_glow_key_set_equal/_glow_map_equal`)+`_mk_stream`+`_ms_digest`+`_META_SMOKE_PATHS/_meta_smoke_cleanup/_meta_new_save`(G9 同名 helper 双份过渡,收尾批去重)+`AutoPlayer` 现路径 preload。全局类裸用:UiKit(微光五组映射);ThemeDB 单例直用。
- **待装配验证要点**:_smoke_global_theme 依赖 ThemeDB 项目主题在座,裁决四要求实跑验证(本批禁跑测试未做)——`--script` 取不到则整体降级系统测试宿主,实跑证据记回本账本。
**G12 单元·源码契约+集成 → 三宿主,35 条迁入,已搬运(待装配验证)。**
- unit/data_config/rng_stream_boot_test.gd(新建):_initialize 13 条——已搬运(待装配验证)。:70「事件日志会话落盘」1 条按裁决一**转办集成**。宿主接线注记:`_initialize` 是 MainLoop 虚拟入口,宿主内改名 `_smoke_initialize` 由 `_init` 显式调用(断言零动)。
- unit/contracts/source_guard_test.gd(新建,裁决五新设子目录):_smoke_type_gate 2+_smoke_hygiene_gates 6——已搬运(待装配验证)。零夹具零 helper(仓库文件直读面)。
- integration/node_map_templates_test.gd(新建,首个集成新增件):_smoke_node_map_templates 14 条(裁决六整套集成不拆)——已搬运(待装配验证)。`_db` 夹具(level.rows 模板池);消费 helper:`_smoke_rest_stub`(自持私有副本——cond 内 `Callable(self, "_smoke_rest_stub")` 引用名照源)。
- **编排壳记注**:_initialize 的编排骨架(load_ok 才进 _run_core_smoke、尾接 _split_tail)、_split_tail 瘦壳、对 _smoke_type_gate/_smoke_hygiene_gates/_smoke_node_map_templates 与我组各套的 _run_core_smoke/_smoke_meta 调用行——**编排非断言,随 smoke_all.gd 收尾整体删除退役**,不搬。
**对账与自检(纯文本)**:
- 六宿主 Kit.check 计数:13+8+14+133+91+120=**379**=名下总条数(390−转办 11)。
- 标签逐字对账:六文件 379 条首参字面量与冻结清单(S01 去 14 号、S76 去 :6807–6831、S36 整章除外)脚本比对**零缺失零多出**(超出抽 5 条要求的全量比对)。
- 粗检:六文件括号/引号平衡、无裸 `check(` 残留;git 状态=仅新增六文件,smoke_all.gd 他人文件零改动。
**转办清单(集成散条,宿主未指定——待主会话/装配批落位)**:
1. _initialize :70「事件日志会话落盘(2026-09-27 统一流裁决)」1 条(裁决一)。
2. _smoke_save ⑨ 摘要集成块基点 :6807–6831 共 10 条(裁决三;消费 `res://smoke/level_autoplay.gd`)。
3. _smoke_playtest_logger 整套 8 条(裁决七;消费 playtest_logger+真实结算流)。
**异常与待主会话**:① _smoke_global_theme ThemeDB 实跑验证缺位(见上,装配验证轮补);② 冻结清单处置列对上述 11 条仍写「迁单元测试」(波 0 生成早于裁决、清单禁改)——以本节转办记录为准;③ 装配验证轮须将六宿主登记进 run_all.gd manifest(含集成件 node_map_templates_test,同入口同协议)。
## 十二、波 1 搬家代理一施工记录(G5 剩余/G6/G7/G8 —— 2026-10-22 搬家代理一)
**作业形态=纯文本搬家**:零测试运行、零 git 提交、`game/smoke/smoke_all.gd` 与他人宿主零改动(名下 31 个套行对应函数与消费 helper 原文全部原地保留,删除归收尾批);manifest(run_all.gd)登记未做(归收尾装配统一登记)。**以下各套状态=已搬运(待装配验证)**。
**G5 单元·封印与极性(剩余七套)→ game/tests/unit/battle_settlement/seal_polarity_test.gd(既有宿主 append-only 追加):67 条迁入,已搬运(待装配验证)。**
- _smoke_seal_irreversible_quadrants(21 条)/_smoke_clear_reward_chain(5 条,源内调用 _smoke_clear_reward_zero_paths 随迁)/_smoke_clear_reward_zero_paths(11 条)/_smoke_refresh_provenance_guard(5 条)/_smoke_polarity_routing(12 条)/_smoke_negative_dictionary(8 条)/_smoke_resource_surface(5 条)——各条已搬运(待装配验证)。
- 追加形态=`_init` 套1 调用行后插六个调用行(套执行序沿源编排基点序:chain→provenance→routing→quadrants→negative→resource——provenance 依赖 chain 写入的 `_regular_clear_reward_rows` 基线)+文件尾追加七套函数与 helper;**套1 既有字节零改动**(git diff=728 insertions / 0 deletions,CRLF 沿用原文件风格)。
- 消费 helper(宿主自持私有副本,按账本「随首用套迁入」随本批首次消费迁入):const `CLEAR_SMOKE_DEPTH/CLEAR_SMOKE_HERO_HP`+成员 `_regular_clear_reward_rows`+const `POSITIVE_EFFECTS/NEGATIVE_EFFECTS`+`_find_seal_target/_auto_play_round/_run_full_clear/_drive_clear/_rows_of_type/_has_source_row/_has_effect_id/_polarity_probe_accepts/_run_polarity_probe/_rows_at`+`_run_battle`(**过渡副本**——去 G6 专属「收束相位回 Idle」断言,G4 同款零断言纯驱动形态);AutoPlayer 复用套1 既有 preload。
- **「他人已完成宿主」处置记账**:seal_polarity_test.gd 是账本 §五 落位计划里 G5 的预定宿主(其文件头注即写明八套),剩余七套唯一去处就是本文件——按「套1 内容勿动」执行 append-only 追加;若主会话要求另立文件请改账并移转。
**G6 单元·轮内流与技能池+变异 → 双宿主,134 条迁入,已搬运(待装配验证)。**
- unit/battle_settlement/round_flow_skill_pool_test.gd(新建):_smoke_full_battle(7 条标签,编排双调用 L1/L2)+_run_battle(**1 条「收束相位回 Idle」本体随迁**——G4/G5/G9 宿主零断言副本以其为本源)+_smoke_active_refresh(17)+_smoke_encounter_roll(8,含全源唯一 quiet=true 一条——Kit 通道无此参,只影响 PASS 行打印,计数/红绿同口径,标签与 cond 逐字保留并加接线注记)+_smoke_settlement_segments(5)+_smoke_skill_pool 调度壳(0 条,源内序调五件)+_smoke_skill_pool_data(27)+_smoke_skill_pool_replay_independence(2)+_smoke_skill_pool_ev_anchor(1)+_smoke_skill_pool_death_void(5)+_smoke_skill_pool_validation_reject(13)+_smoke_debuff_rider(3)+_smoke_counter_buckets(6)=95 条。套执行序沿源编排基点调用序(full_battle×2→active_refresh→encounter_roll→settlement_segments→skill_pool 壳→counter_buckets)。
- unit/battle_settlement/variant_placement_test.gd(新建):_smoke_variant_placement(9 条标签,编排双调用 L1/L2)+_smoke_global_variant(30)=39 条。
- 消费 helper(各自持私有副本):round_flow=`_auto_play_round/_active_refresh_replay/_row_kind_of/_boss_depth/_first_encounter_band/_level_active_nodes/_level_battle_nodes/_encounter_replay/_row_phase/_fresh_rng/_record_pool_trajectories/_has_err_category`+AutoPlayer 现路径 preload+全局类裸用 SkillLottery/SettlementTable/EffectEngine(G3 先例);variant_placement=class `_CountVariantRng`(extends RngServiceCore)+`_variant_dump/_arr_str_eq/_rows_weights_equal/_rows_weight_map/_board_resource_dump/_errs_has_cross_field`。
**G7 单元·冒险结构与专长 → game/tests/unit/core/adventure_feats_test.gd(新建):133 条迁入,已搬运(待装配验证)。**
- _smoke_adventure_api(11)/_smoke_adventure(54)/_smoke_feats(68=源 71−转办集成 3)——已搬运(待装配验证)。
- 剪切记账:_smoke_feats 断点往返块(基点 :1420–1426 三条+其 save_manager 驱动代码)按裁决②整体移出宿主、宿主内留转办注记;表还原行 `_db.feats = live_rows` 原样保留(后续 V3 订阅面断言依赖现行表)。
- 消费 helper(自持私有副本):`_smoke_find_token/_api_mark_started/_api_mark_choices/_api_mark_node/_adv_drive_to_shop/_adv_st/_battle_st/_fresh_rng/_level_active_nodes`;`res://smoke/level_autoplay.gd` 按现路径局部 load;全局类裸用 FeatTable/EffectEngine。
**G8 单元·关卡图与 GP 商店 → game/tests/unit/core/level_shop_economy_test.gd(新建):141 条迁入,已搬运(待装配验证)。**
- _smoke_level(11)/_smoke_level_graph(54)/_smoke_gp_wallet(9)/_smoke_gp_escrow(12)/_smoke_shop_room(22)/_smoke_shop_graph(6)/_smoke_shop_pool(27)——已搬运(待装配验证)。
- 消费 helper(自持私有副本):`_battle_st/_fresh_rng/_escrow_sum_of/_find_gp_dig/_shop_products_by_id/_adv_drive_to_shop/_adv_st/_all_entry_boss_paths/_level_active_nodes/_first_encounter_band/_boss_depth/_has_err_category`;`res://smoke/level_autoplay.gd` 按现路径局部 load。
**对账与自检(纯文本)**:
- 宿主 Kit.check 计数:seal 追加 67(+套1 既有 5=文件共 72)+round_flow 95+variant_placement 39+adventure_feats 133+level_shop_economy 141=**475**=名下总条数(478−转办 3)。
- 标签/cond 全量脚本比对(超出抽 5 条要求):五宿主 475 条按被迁函数逐条与 smoke_all.gd 源对齐(label/cond 折叠口径零差异;_smoke_feats 按剪切后 68 vs 源 71 对齐);475 条标签字面量与冻结清单各套章节逐字比对零缺失零多出。
- 粗检:四新建文件无裸 `check(` 残留(round_flow 仅一处源注释「禁恒真 check(true)」随套逐字带入);文件尾换行齐。
**转办清单(集成散条,单元宿主不收——待装配批落位)**:
1. _smoke_feats 基点 :1420–1426 三条(裁决②):「断点附随件:relics 持有集入快照(迭代 12 T1 扩列)」「断点附随件:adventure 计数器账本键在案(T6/T7 扩列)」「断点余库:已兑付 relic 令牌逐枚还原(x3 零残留——续玩不重复获得)」——驱动代码=save_manager 临时档(`user://_smoke_feats_bp.json`)装配+注入表上 `_on_level_finished(victory)`+`breakpoint_snapshot()` 取读+`clear_breakpoint()`,随三条同迁(adventure_feats_test.gd 内已留注记)。
**异常与待主会话**:① 「他人已完成宿主一个字不许改」与「G5 剩余套落位 seal_polarity_test.gd」字面冲突,按 append-only 执行(见上 G5 节),请主会话追认或改判;② 冻结清单处置列对 feats 三条仍写「迁单元测试」(波 0 生成早于裁决、清单禁改)——以本节转办记录为准;③ 我批被迁函数与共享 helper 在 smoke_all.gd 原地保留(本波禁改 smoke_all),其中 `_find_seal_target/_run_full_clear/_drive_clear/_rows_of_type/_has_source_row/_has_effect_id/_run_polarity_probe/_polarity_probe_accepts/_rows_at` 与两 const 的 smoke 侧消费者已全部迁毕,原件删除归收尾批;④ 跨宿主重复 helper(收尾去重候选):`_fresh_rng`×3(rf/af/ls)+`_has_err_category`×2(rf/ls)+`_level_active_nodes`×3(rf/af/ls)+`_first_encounter_band`×2(rf/ls)+`_boss_depth`×2(rf/ls)+`_battle_st`×2(af/ls)+`_adv_st`×2(af/ls)+`_adv_drive_to_shop`×2(af/ls)+`_auto_play_round/_run_battle` 过渡副本多宿主(rf 主持本体带断言,G4/G5/G9 副本零断言);⑤ 装配验证轮须将四件新宿主登记进 run_all.gd manifest。
## 十三、装配批施工记录(2026-10-22 装配代理)
**作业形态=纯文本与 python 级装配**:零游戏测试(引擎由验证线并行占用)、零 git 提交、M1/M2 十一件已迁宿主内容零改动(seal_polarity_test.gd diff 终检仍=728 insertions/0 deletions,即 M1 交付原态)。以下装配结果=待统一验证,实跑归主会话。
**1. 转办 22 条落集成宿主**:新建 `game/tests/integration/cross_system_flow_test.gd`(命名照 integration 现有惯例;与单元同入口同协议=extends SceneTree+Kit.check 主通道;表加载失败不中止承 G2–G5 同款;驱动随条自持、零新增断言——前置受理失败自然落后续断言红)。四块去向(四宿主注记所指转办义务至此全部落位):
| 转办块 | 条数·裁决 | 宿主函数 | 驱动形态 |
|---|---|---|---|
| _initialize :70 事件日志会话落盘 | 1·裁决一 | `_rng_event_log_session` | 沿源取号序列自持:set_master_seed(42)→open_stream×2→new_adventure(4242)→open_stream("gp_drop")→查 `user://logs/rng_log_4242.jsonl` |
| _smoke_feats 基点 :1420–1426 断点往返 | 3·裁决二 | `_feats_breakpoint_roundtrip` | 沿源 :721–797 重建:5 行临时表注入+seed 72 起局+兑付 x3+save_manager 临时档(`user://_smoke_feats_bp.json`)装配+_on_level_finished(victory)+快照取读+clear_breakpoint;起局/兑付两步受理断言留单元宿主;临时档段首清场+段尾清理;表还原行保留 |
| _smoke_save ⑨ 摘要集成块 基点 :6807–6831 | 10·裁决三 | `_save_summary_integration` | level_autoplay 按现路径 preload;`user://smoke_save_integration.json`(源 paths[8])段首清场+段尾清理,绝不碰 user://save.json |
| _smoke_playtest_logger 整套 | 8·裁决七 | `_smoke_playtest_logger`(函数名沿源) | PlaytestLogger×BattleOrchestrator 真实结算流驱动照源(快照信号收集/停手两结算/诊断转储/JSONL 回读/close_session) |
**2. manifest 登记**:`game/tests/run_all.gd` `_suites()` 追加 11 行(44→55 条,expect 全 0),格式照现行惯例(域分组插入+注释枚举覆盖面+「批 B 自 smoke_all 迁入」尾注):unit 九件新=round_flow_skill_pool/variant_placement/adventure_feats/level_shop_economy/meta_progression_forge/save_manager_domain/theme_projection/rng_stream_boot/source_guard;integration 两件=node_map_templates/cross_system_flow(转办块宿主)。既有四件登记(table_structure_guard/board_gen_reward/roll_multiplier/seal_polarity)未重。
**3. 闸 01 切换**:`tools/run_system_test.py` DEFAULT_SCRIPT 改 `res://tests/system_test.gd`(头注宿主句与用法注释同步;闸 01 识别的两形态摘要行不变);`tools/run_all_gates.sh` 闸 01 注释改指新宿主,闸键名「01 系统测试」未动。
**4. 删源**:`game/smoke/smoke_all.gd`+`smoke_all.gd.uid` 整体删除(两件均入 git 历史,工作区 ` D` 未暂存,与两搬家代理未提交产出同批待主会话统一提交)。
**5. extract_checks.py 改造与自检**:默认源改三宿主遍历(unit/**+integration/** 递归+system_test.gd,现命中 58 文件);`--src` 升级多文件显式模式(相对路径按仓库根解析);解析器双通道=点前缀 `Kit.check(` 标签在前/裸 `check(` cond 在前(`_check(` 同名前缀与标签实参无字面量的 wrapper 调用不取——先例 reward_overlay_test `_req` 直传变量;字面量损坏仍会以「消失」呈现,不漏检);`--extract`/`--list-funcs` 随多源升级(多源按〔源文件,函数〕分章,章头带源);冻结清单防误覆不变。对账指纹按合同 §五三要素(标签/cond/计数)修正——quiet 列保留展示不入指纹:quiet 是 smoke 通道打印档位(只影响 PASS 行打印,计数/红绿同口径,M1 已记账),Kit 通道无此参,波 0 四元指纹在通道转换后结构性失配;清单冻结三列未动一字,非放宽对账(quiet 变更在 smoke 通道已无承载面)。
自检两口径(python 级,非游戏测试):
- **显式模式(16 宿主=本批迁移面全量):清单 1065/现提取 1065 计数对齐,标签/cond 逐字零差异,PASS(退出码 0)**——1043 已搬+22 转办全在宿主。分件计数与 §二/§五落位逐件相等:system_test 49/table_structure_guard 60/rng_stream_boot 13/source_guard 8/board_gen_reward 39/adventure_feats 133/level_shop_economy 141/meta_progression_forge 133/save_manager_domain 91/roll_multiplier 36/seal_polarity 72/round_flow_skill_pool 95/variant_placement 39/theme_projection 120/node_map_templates 14/cross_system_flow 22。
- **缺省遍历模式:消失 0/标签变更 0/cond 变更 0;新增 317 条全部来自既有套件**(config_db_test 34、damage_controller_test 34、type_glow_test 23、level_map_type_vis_test 23、damage_math_test 21、forge_table_test 20 等 22 文件——全为批前在册单元/集成套件的 Kit.check 断言,非本批迁移面;16 宿主在新增列零出现)。退出码 1 系「新增」类差异,属遍历面含既有套件的预期形态;批 B 出口判定以显式模式为准(§十.1「波 3 出口对账用显式模式」同口径)。
**待实跑(归下一棒验证代理/统一验证)**:
- 裁决四 `_smoke_global_theme` ThemeDB 项目主题 `--script` 取得性验证(theme_projection_test 宿主内注记同此)——取不到则整体降级系统测试宿主,实跑证据记回账本。
- 全部宿主与闸 01/02 实跑归统一验证(本批零游戏测试);manifest 新 11 件 expect 全 0(无致命/负面类)。
- 工具与文档面遗留(§七普查在案,归波 2):`game/tests/static_scan.sh:100` S11 头注仍提 smoke_all(纯注释);`game/tests/README.md` 过渡期口径句(含 manifest 条数句现为 55)、AGENTS.md:20、architecture.md、game/README 等引用面同批换代;manifest 登记的 11 件新宿主 .uid 边车由引擎首轮导入自动生成(与 M1/M2 九件新件同态,禁手造)。
## 十四、核代码修复批施工记录(2026-10-23 主会话执行)
三冷启动审核员 1065 条全审:1047 有效 / 17 条逐条存疑 + 2 段落级(后者经今日闸记录实证解决:system_test.log 65 项 0 失败=48 条 autoload 门控断言全实跑;unit.log「V1 项目主题挂载」PASS=ThemeDB 在座 21 条全实跑)。grilling 审查一轮:5 条方案成立、4 条被击穿修订(A1 收敛为 has 键在案/B7 改夹具观测路线并补 hp_acc 项/C9 落锤现态加场景绑定/B8-区间行改判测试侧死逻辑——「生产疑似 bug」系 G3 错账已撤销)、另按升格标准新增 attach/rider 键名修复。10 条全部由主会话执行完毕:
1. cross_system_flow:92 恒真→`bp_r.has("adventure_counters")`(键在案语义;该流零计数器内容,禁验 size>0)
2. table_guard:493 前置 derive_at×3 调用,使命题「纯函数不推计数器」真正被验
3. table_guard:130 拆两条:null 早退分支(label 补注)+ _TreeSaveMock 非 null 档真实计数(socket1=1)
4. theme:554/580/596 对象引用比较→内容比较(新增 `_sb_content_differs` 助手:Texture 比纹理/Flat 比 bg_color/异型即不同)
5. table_guard 护栏后新增绊线钉:hero_defense 池映射数==0
6. seal:481-488 补 ap_delta 按行值符号辨极性分支(与安全格循环同口径;_polarity_probe_accepts 未动;:537 同形缺口靠受理谓词保护的依赖已记)
7. level_autoplay 停点加 bonus_max_hp 观测面(测试基建)+ adventure_feats:185-210 窗口拟合改观测值精确断言(公式含 hp_acc 项)
8. board_gen:270-275 自比较死逻辑重写:新增 draws 总抽数计数,判据改「总抽≥2 且不同值<2」
9. level_shop:806-818 白盒抬 GP+64 绑定场景+else 显式红护栏
10. roll_multiplier:443 "attach"→"rider" 键名修复
**闭案措辞(按测试规约 §6)**:system_test :279/:421 与幂等等值类=「结构复核通过、变异拦截能力未证(执行前提/预算收刀)」。**变异形态留档(只登记不执行——测试已被用户叫停)**:1=adventure_runner.gd:1025 键改名|2=derive_at 内推计数器|3b=计数循环恒 0|4=disabled 改挂 pressed 同内容|5 钉=effect_pool_map 加 hero_defense 映射|6=seal 兑付行符号翻转|7=回血基数改表值上限|8=双掷点恒取下界(R1 已实证形态)|9=货架全不可负担|10=Boss 池加入 rider_mult 技能。**错账撤销**:G3「生产疑似 bug 登记待办」作废——区间行死逻辑在测试源码自身,本批已修。
+137 -69
View File
@@ -1,24 +1,30 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
r"""extract_checks.py — smoke_all.gd 断言静态提取器(验证集清单生成与出口对账)
r"""extract_checks.py — 断言静态提取器(验证集清单生成与出口对账)
机器提取 game/smoke/smoke_all.gd 的全部静态 check( 调用点(每个调用点记录:所在函数、
机器提取宿主脚本的全部静态 check( 调用点(每个调用点记录:所在文件、所在函数、
行号、标签字符串字面量原文、cond 表达式原文),提供两种模式:
用法:
python tools/extract_checks.py --extract [--src <源文件>] [--out <清单文件>] [--force]
python tools/extract_checks.py --extract [--src <源文件>...] [--out <清单文件>] [--force]
生成验证集清单(已存在时拒写,除非 --force——冻结参照物防误覆)
python tools/extract_checks.py --diff <清单文件> [--src <源文件>]
python tools/extract_checks.py --diff <清单文件> [--src <源文件>...]
对当前源码再提取,与清单逐条比对——批 B 出口对账唯一工具
python tools/extract_checks.py --list-funcs [--src <源文件>]
python tools/extract_checks.py --list-funcs [--src <源文件>...]
只打印函数清单与各函数条数(编账本辅助,不写文件)
断言源(--src 可多个;缺省=三宿主遍历):
game/tests/unit/**/*.gd + game/tests/integration/**/*.gd + game/tests/system_test.gd
批 B 出口对账用显式模式(--src 列全本批宿主文件)——缺省遍历含既有套件的 Kit.check
断言,对账时以「新增」出现(非迁移面),出口判定以显式模式为准。
提取口径:
- 静态 check( 调用点。与行级 grep(grep -c "check(")的差:grep 数含 func check 定义行
与一处注释行(「禁恒真 check(true)」),行级 grep=1067、真实调用点=1065(2026-10-22 基点实测)。
- 静态 check( 调用点,两种通道都认:smoke/系统宿主的 check(cond, label)(cond 在前)
与单元/集成宿主的 Kit.check("label", cond)(点前缀=标签在前);`_check(` 等同名
前缀调用非标准通道,不取。
- 调用点可跨行:从 `check(` 起括号配平归集完整实参文本(() [] {} 三种括号都计深度);
字符串字面量内的括号/逗号/井号、行尾注释、转义序列均不参与结构判定(码位掩码单趟预扫)。
- cond 摘要=cond 原文跨行折叠为单行(连续空白并一空格);标签原文=第二个实参里的字符串
- cond 摘要=cond 原文跨行折叠为单行(连续空白并一空格);标签原文=标签实参里的字符串
字面量逐字保真(含引号与转义序列原样,禁改写);标签实参若带格式化/拼接后缀
(`"..." % [...]`),后缀折叠后以「↩」接排在标签列。
- md 表格单元格内 `|` 转义为 `\|`、`\` 转义为 `\\`(源码串内无这两序列的歧义形态,读回可还原)。
@@ -37,9 +43,26 @@ import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
DEFAULT_SRC = ROOT / "game" / "smoke" / "smoke_all.gd"
# 断言源缺省=三宿主遍历:unit/**+integration/** 两目录递归 + 系统宿主单文件。
# 遍历面含既有套件的 Kit.check 断言(非本批迁移面,对账呈「新增」)——出口对账用 --src 显式列宿主。
DEFAULT_TREES = (
ROOT / "game" / "tests" / "unit",
ROOT / "game" / "tests" / "integration",
ROOT / "game" / "tests" / "system_test.gd",
)
DEFAULT_OUT = ROOT / "game" / "tests" / "验证集清单.md"
def default_sources():
"""三宿主遍历:两目录递归 *.gd + 系统宿主单文件,路径排序定序。"""
files = []
for p in DEFAULT_TREES:
if p.is_file():
files.append(p)
elif p.is_dir():
files.extend(sorted(p.rglob("*.gd")))
return sorted(files)
CALL_RE = re.compile(r"check\(")
DEF_RE = re.compile(r"^\s*(static\s+)?func\s+check\s*\(")
FUNC_RE = re.compile(r"^(static\s+)?func\s+([A-Za-z_][A-Za-z0-9_]*)")
@@ -204,7 +227,7 @@ def collect_call(joined, start):
def extract(src_path: Path):
"""提取全部静态 check( 调用点。返回(条目列表, 函数表),条目保持源码顺序。"""
"""提取单文件全部静态 check( 调用点。返回(条目列表, 函数表),条目保持源码顺序。"""
src = src_path.read_text(encoding="utf-8")
lines = src.split("\n")
funcs = func_table(lines)
@@ -227,21 +250,28 @@ def extract(src_path: Path):
ln = line_of(m.start())
if DEF_RE.match(lines[ln - 1]):
continue # func check 定义行
prev = joined[m.start() - 1] if m.start() > 0 else ""
if prev and (prev.isalnum() or prev == "_"):
continue # `_check(` 等同名前缀——非标准通道
label_first = prev == "." # Kit.check("label", cond)——点前缀=标签在前
body, end_pos = collect_call(joined, m.start())
if body is None:
raise SystemExit("[extract_checks] FAIL:行 %d 起的 check( 括号不配平到文件尾——先查解析缺口" % ln)
raise SystemExit("[extract_checks] FAIL:%s 行 %d 起的 check( 括号不配平到文件尾——先查解析缺口"
% (src_path.name, ln))
args = split_args(body)
if len(args) not in (2, 3):
raise SystemExit(
"[extract_checks] FAIL:行 %d 的 check( 拆出 %d 个实参(预期 2/3)——解析缺口,先查再跑"
% (ln, len(args)))
cond = args[0]
label_expr = args[1]
"[extract_checks] FAIL:%s 行 %d 的 check( 拆出 %d 个实参(预期 2/3)——解析缺口,先查再跑"
% (src_path.name, ln, len(args)))
if label_first:
label_expr, cond = args[0], args[1]
else:
cond, label_expr = args[0], args[1]
quiet = args[2] if len(args) == 3 else ""
lm = LITERAL_RE.search(label_expr)
if lm is None:
raise SystemExit(
"[extract_checks] FAIL:行 %d 的标签实参不含字符串字面量——解析缺口,先查再跑" % ln)
continue # 标签实参无字面量(wrapper 直传变量——既有套件先例 reward_overlay_test _req)
# 不取:无可指纹化面;冻结面对账不受影响(字面量损坏会以「消失」呈现)
entries.append({
"line": ln,
"end_line": line_of(min(end_pos, len(joined) - 1)),
@@ -254,6 +284,20 @@ def extract(src_path: Path):
return entries, funcs
def extract_all(src_paths):
"""多文件提取合并。返回(条目列表, {相对路径: 函数表});条目附加 src=相对路径。"""
entries = []
funcs_by_src = {}
for p in src_paths:
es, funcs = extract(p)
rel = p.relative_to(ROOT).as_posix() if p.is_relative_to(ROOT) else p.as_posix()
for e in es:
e["src"] = rel
entries.extend(es)
funcs_by_src[rel] = funcs
return entries, funcs_by_src
def label_cell(e):
"""标签列:字面量 ↩ 后缀(后缀空则只有字面量)。"""
sfx = fold(e["label_suffix"])
@@ -261,8 +305,10 @@ def label_cell(e):
def entry_key(e):
"""对账指纹:cond 折叠 + 标签字面量(逐字)+ 后缀折叠 + quiet。"""
return (fold(e["cond"]), e["label_literal"], fold(e["label_suffix"]), fold(e["quiet"]))
"""对账指纹=合同 §五三要素:cond 折叠 + 标签字面量(逐字)+ 后缀折叠。
quiet 不入指纹——它是 smoke 通道的打印档位(只影响 PASS 行打印,计数/红绿同口径),
Kit 通道无此参,迁移时按接线注记落通道转换;quiet 列保留展示,不参与对账。"""
return (fold(e["cond"]), e["label_literal"], fold(e["label_suffix"]))
def pad3(n):
@@ -280,13 +326,14 @@ def base_commit():
return "<git 不可用,未取得基点 commit>"
def render_list(entries, funcs, src_rel: str) -> str:
"""生成验证集清单骨架。白话释义/处置两列留待回填(回填不动冻结列)。"""
def render_list(entries, funcs_by_src, src_names) -> str:
"""生成验证集清单骨架。白话释义/处置两列留待回填(回填不动冻结列)。
多源按(源文件, 函数)分章——跨文件同名函数不并章;章头行号=该源文件内行号。"""
out = []
out.append("# 验证集清单(迁移参照物·冻结)")
out.append("")
out.append("> 生成日期:2026-10-22 · 基点 commit:`%s` · 源:`%s`" % (base_commit(), src_rel))
out.append("> 提取口径:静态 `check(` 调用点,括号配平归集(可跨行)。基点实测调用点 1065 处;")
out.append("> 生成日期:2026-10-22 · 基点 commit:`%s` · 源:%s" % (base_commit(), "、".join(src_names)))
out.append("> 提取口径:静态 `check(` 调用点,括号配平归集(可跨行)。批 B 基点实测 smoke_all.gd 调用点 1065 处;")
out.append("> 行级 `grep -c \"check(\"` = 1067 是另一口径——多出的 2 行为 `func check(` 定义行与")
out.append("> 一处注释行(「禁恒真 check(true)」),非调用点。运行时计数大于静态计数=循环展开条目,")
out.append("> 两口径各自浮动,禁互相换算(tests/README「断言计数双口径」)。")
@@ -294,20 +341,21 @@ def render_list(entries, funcs, src_rel: str) -> str:
out.append("> 冻结纪律:每条的「标签原文」「cond 摘要」「quiet」三列为机器提取逐字保真,禁任何改写;")
out.append("> cond 摘要=原文跨行折叠单行(连续空白并一空格),单元格内 `|`→`\\|`、`\\`→`\\\\` 转义,读回还原。")
out.append("> 「白话释义」「处置」两列由维护回填,不属冻结面。")
out.append("> 出口对账:`python tools/extract_checks.py --diff game/tests/验证集清单.md`")
out.append("> 出口对账:`python tools/extract_checks.py --diff game/tests/验证集清单.md --src <宿主文件>...`")
out.append("> ——标签/cond/计数三对齐才算迁移完(合同 §五出口机检)。")
out.append("> 维护义务:此后断言新增/删除/改判必须同步本清单(test-conventions §9.3 锚)。")
out.append("")
seq = 0
by_func = []
by_gf = []
for e in entries:
if by_func and by_func[-1][0] == e["func"]:
by_func[-1][1].append(e)
key = (e["src"], e["func"])
if by_gf and by_gf[-1][0] == key:
by_gf[-1][1].append(e)
else:
by_func.append((e["func"], [e]))
for idx, (fn, es) in enumerate(by_func, 1):
by_gf.append((key, [e]))
for idx, ((src, fn), es) in enumerate(by_gf, 1):
start, end = es[0]["line"], es[-1]["end_line"]
out.append("## %s %s(行 %d–%d · %d 条 · 去向:待分类回填)" % (pad3(idx), fn, start, end, len(es)))
out.append("## %s %s(行 %d–%d · %d 条 · 源 %s · 去向:待分类回填)" % (pad3(idx), fn, start, end, len(es), src))
out.append("")
out.append("| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet |")
out.append("|---|---|---|---|---|---|---|")
@@ -317,7 +365,7 @@ def render_list(entries, funcs, src_rel: str) -> str:
seq, e["line"], esc_cell(label_cell(e)), esc_cell(fold(e["cond"])), esc_cell(fold(e["quiet"]))))
out.append("")
out.append("---")
out.append("全源共 %d 章、%d 条(静态调用点口径)。" % (len(by_func), seq))
out.append("全源共 %d 章、%d 条(静态调用点口径)。" % (len(by_gf), seq))
return "\n".join(out) + "\n"
@@ -361,8 +409,8 @@ def _short(s, n=80):
return s if len(s) <= n else s[:n] + "…"
def diff_report(list_path: Path, src_path: Path) -> int:
cur, _funcs = extract(src_path)
def diff_report(list_path: Path, src_paths) -> int:
cur, _funcs = extract_all(src_paths)
frozen = parse_list(list_path)
cur_keys = {}
for e in cur:
@@ -377,7 +425,7 @@ def diff_report(list_path: Path, src_path: Path) -> int:
used_added = set()
rest_missing = []
for k in missing:
cond, lit, sfx, _q = k
cond, lit, sfx = k
pair = next((ka for ka in added if ka not in used_added and ka[0] == cond), None)
if pair is not None:
used_added.add(pair)
@@ -391,12 +439,12 @@ def diff_report(list_path: Path, src_path: Path) -> int:
rest_missing.append(k)
rest_added = [k for k in added if k not in used_added]
ok = True
print("[extract_checks] 对账:%s vs 现提取 %s" % (list_path.name, src_path.name))
print("[extract_checks] 对账:%s vs 现提取 %d 个源文件" % (list_path.name, len(src_paths)))
print(" 清单条数=%d 现提取条数=%d %s" % (
len(frozen), len(cur), "(计数对齐)" if len(frozen) == len(cur) else "(计数不齐)"))
def show(k, tag):
cond, lit, sfx, q = k
def show(k, tag, q=""):
cond, lit, sfx = k
s = " %s cond=%s label=%s" % (tag, _short(cond), _short(lit))
if sfx:
s += " suffix=%s" % _short(fold(sfx))
@@ -408,27 +456,27 @@ def diff_report(list_path: Path, src_path: Path) -> int:
ok = False
print(" 标签字面变更 %d 对(cond 同、标签异):" % len(label_changes))
for old, new in label_changes:
show(old, "-")
show(new, "+")
show(old, "-", frozen_keys[old][0].get("quiet", ""))
show(new, "+", cur_keys[new][0].get("quiet", ""))
if cond_changes:
ok = False
print(" cond 字面变更 %d 对(标签同、cond 异):" % len(cond_changes))
for old, new in cond_changes:
show(old, "-")
show(new, "+")
show(old, "-", frozen_keys[old][0].get("quiet", ""))
show(new, "+", cur_keys[new][0].get("quiet", ""))
if rest_missing:
ok = False
print(" 消失 %d 条(清单有、现码无):" % len(rest_missing))
for k in rest_missing:
print(" - [%s]" % frozen_keys[k][0].get("section", "?"), end="")
show(k, "")
show(k, "", frozen_keys[k][0].get("quiet", ""))
if rest_added:
ok = False
print(" 新增 %d 条(现码有、清单无):" % len(rest_added))
for k in rest_added:
es = cur_keys[k][0]
print(" + [func %s @%d]" % (es["func"], es["line"]), end="")
show(k, "")
print(" + [src %s func %s @%d]" % (es["src"], es["func"], es["line"]), end="")
show(k, "", es.get("quiet", ""))
if ok:
print("[extract_checks] PASS——标签/cond/计数三对齐,零差异")
return 0
@@ -436,57 +484,77 @@ def diff_report(list_path: Path, src_path: Path) -> int:
return 1
def list_funcs(entries, funcs):
by_func = []
for e in entries:
if by_func and by_func[-1][0] == e["func"]:
by_func[-1][1] = by_func[-1][1] + 1
else:
by_func.append([e["func"], 1])
checked = {f for f, _ in by_func}
for fn, n in by_func:
print("%s\t%d" % (fn, n))
print("--- 无断言函数(helper,随套迁移)---")
for fn, ln in funcs:
if fn not in checked:
print("%s\t@%d" % (fn, ln))
def list_funcs(entries, funcs_by_src):
for src, funcs in funcs_by_src.items():
by_func = []
for e in entries:
if e["src"] != src:
continue
if by_func and by_func[-1][0] == e["func"]:
by_func[-1][1] = by_func[-1][1] + 1
else:
by_func.append([e["func"], 1])
print("=== %s ===" % src)
checked = {f for f, _ in by_func}
for fn, n in by_func:
print("%s\t%d" % (fn, n))
print("--- 无断言函数(helper,随套迁移)---")
for fn, ln in funcs:
if fn not in checked:
print("%s\t@%d" % (fn, ln))
def resolve_sources(src_args):
"""--src 列表 → Path 列表(相对路径按仓库根解析);缺省=三宿主遍历。任一文件不可读即 None。"""
if src_args:
paths = [Path(s) if Path(s).is_absolute() else ROOT / s for s in src_args]
else:
paths = default_sources()
if not paths:
print("[extract_checks] FAIL:断言源为空(三宿主遍历无命中?)")
return None
for p in paths:
if not p.is_file():
print("[extract_checks] FAIL:源文件不可读:%s" % p)
return None
return paths
def main() -> int:
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
ap = argparse.ArgumentParser(description="smoke_all.gd 断言静态提取器(清单生成/对账)")
ap = argparse.ArgumentParser(description="断言静态提取器(清单生成/对账)")
ap.add_argument("--extract", action="store_true", help="生成验证集清单")
ap.add_argument("--diff", metavar="清单文件", help="与清单逐条对账(批 B 出口机检)")
ap.add_argument("--list-funcs", action="store_true", help="打印函数清单与条数(编账本辅助)")
ap.add_argument("--src", default=str(DEFAULT_SRC), help="断言源文件")
ap.add_argument("--src", nargs="+", default=None,
help="断言源文件(可多个;缺省=三宿主遍历 unit/**+integration/**+system_test.gd)")
ap.add_argument("--out", default=str(DEFAULT_OUT), help="--extract 输出路径")
ap.add_argument("--force", action="store_true", help="输出文件已存在时允许覆盖")
args = ap.parse_args()
if sum(bool(x) for x in (args.extract, args.diff, args.list_funcs)) != 1:
print("[extract_checks] 用法:--extract / --diff <清单文件> / --list-funcs 三选一")
return 2
src_path = Path(args.src)
if not src_path.is_file():
print("[extract_checks] FAIL:源文件不可读:%s" % src_path)
src_paths = resolve_sources(args.src)
if src_paths is None:
return 2
if args.extract:
out = Path(args.out)
if out.exists() and not args.force:
print("[extract_checks] FAIL:清单已存在(%s)——冻结参照物防误覆,重生成须加 --force" % out)
return 2
entries, _funcs = extract(src_path)
out.write_text(render_list(entries, _funcs, src_path.name), encoding="utf-8", newline="\n")
print("[extract_checks] 已生成 %s(%d 个调用点)" % (out, len(entries)))
entries, funcs_by_src = extract_all(src_paths)
out.write_text(render_list(entries, funcs_by_src, [p.name for p in src_paths]), encoding="utf-8", newline="\n")
print("[extract_checks] 已生成 %s(%d 个源文件、%d 个调用点)" % (out, len(src_paths), len(entries)))
return 0
if args.diff:
lp = Path(args.diff)
if not lp.is_file():
print("[extract_checks] FAIL:清单不可读:%s" % lp)
return 2
return diff_report(lp, src_path)
entries, funcs = extract(src_path)
list_funcs(entries, funcs)
return diff_report(lp, src_paths)
entries, funcs_by_src = extract_all(src_paths)
list_funcs(entries, funcs_by_src)
return 0
+1 -1
View File
@@ -163,7 +163,7 @@ gate_unit() { # run_all 是 --script 模式,退出码为唯一判据
if in_domain code; then
# ── 0. 冒烟测试(极小启动场景:真实开机路径存活检查,零逻辑判定,秒级)──
gate "00 冒烟测试" "smoke_boot.log" "$PY" "$REPO/tools/run_smoke.py"
# ── 1. 系统测试(真实玩法路径断言面;过渡期宿主=game/smoke/smoke_all.gd,断言迁移归批 B)──
# ── 1. 系统测试(真实玩法路径断言面;宿主=game/tests/system_test.gd,缺省 --script 由 run_system_test.py 持有)──
gate "01 系统测试" "system_test.log" "$PY" "$REPO/tools/run_system_test.py"
# ── 2. 单元与集成测试 ──
gate_unit
+4 -6
View File
@@ -2,14 +2,12 @@
r"""run_system_test.py — 系统测试运行器(脚本错误外部门禁)
用法:
python tools/run_system_test.py # 跑系统测试(过渡期宿主 res://smoke/smoke_all.gd)
python tools/run_system_test.py --script res://tests/system_test.gd
python tools/run_system_test.py # 跑系统测试(宿主 res://tests/system_test.gd)
python tools/run_system_test.py --script <宿主脚本>
python tools/run_system_test.py --godot <引擎路径> --path <切片目录> --timeout 300
系统测试 = 启动真实工程走真实玩法路径的断言面(标准四级测试层之一)。
宿主过渡态:断言现全部住在 res://smoke/smoke_all.gd,按「只剪切禁改写」纪律迁往
res://tests/system_test.gd(宿主薄壳已在位)——迁移完成后本件缺省 --script 改指新宿主,
闸 01 系统测试随迁。
宿主:res://tests/system_test.gd(--script 薄壳自持 check 通道;断言面自 smoke_all.gd 迁入——沿革查 git log)。
背景: GDScript 运行时脚本错误(SCRIPT ERROR)不改变进程退出码、不计入失败数,
且会静默中断所在断言函数的剩余断言——摘要零失败也可能断言未跑全。
@@ -29,7 +27,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
DEFAULT_GODOT = r"D:\code\godot\Godot_v4.7.2-stable_win64_console.exe"
DEFAULT_SLICE = ROOT / "game"
DEFAULT_SCRIPT = "res://smoke/smoke_all.gd" # 过渡期宿主;断言迁移批后改 res://tests/system_test.gd
DEFAULT_SCRIPT = "res://tests/system_test.gd" # 系统测试宿主
SUMMARY_RE = re.compile(r"(?:冒烟完成|\[system_test\] 摘要):(\d+) 项检查,(\d+) 项失败")
ERROR_MARKERS = ("SCRIPT ERROR", "Parse Error")