T6 探针基建收口段: 三族样板收单点(probe_ui_util树寻址contains/begins_with参数化+isolate_save七份隔离样板/battle_driver五份战斗驱动)+level_autoplay双战斗循环合并_decide_act+test_kit收13套件_check样板(断言数逐套件零丢失对照表在报告)+BAD_DIR夹具共享+smoke_all五件(:953公有面clamped_pool/_battle_st七袋/check_silent并入quiet参/_pct转发删/load改preload);探针小件=main句柄Node类型注/魔法等待常量化/画布常数同源锚注/死参verbose删;红线守住=ui_input case④b与click循环体零触碰/拍板点三件未退役

This commit is contained in:
DKChan committed 2026-10-02 08:46:43 +08:00
1 parent 0691a188fd
commit 315e46038c
32 files changed
+587 -764

No files matched your search

+15 -27
View File
@@ -6,13 +6,7 @@
extends SceneTree
const AuxItemsScript := preload("res://core/aux_items.gd")
var _problems: Array = []
func check(cond: bool, msg: String) -> void:
if not cond:
_problems.append(msg)
const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:check/汇总退出样板单点(静默收集通道)
class StubTable:
@@ -59,39 +53,39 @@ func _init() -> void:
var inv = _new_inv()
var r1: Dictionary = inv.add_item("sapper_order", "safe_cell", Vector2i(3, 4), 2)
var r2: Dictionary = inv.add_item("nest_pendulum", "flag_hit", Vector2i(5, 6), 3)
check(int(r1.handle) == 1 and int(r2.handle) == 2, "句柄受理序单调(1→2——零随机)")
check(String(r1.source_class) == "safe_cell" and r1.origin_cell == Vector2i(3, 4) and int(r1.round_index) == 2,
Kit.check_quiet(int(r1.handle) == 1 and int(r2.handle) == 2, "句柄受理序单调(1→2——零随机)")
Kit.check_quiet(String(r1.source_class) == "safe_cell" and r1.origin_cell == Vector2i(3, 4) and int(r1.round_index) == 2,
"授予来源可归因(source/cell/round)")
var inv_proj: Array = inv.query_inventory(Callable())
check(inv_proj.size() == 2 and not bool(inv_proj[0].used), "库存投影两件未用")
check(bool(inv.consume(int(r1.handle)).get("used", false)), "consume 返回已用快照回执")
check(inv.consume(int(r1.handle)).is_empty(), "Used 不可逆——二次 consume 拒绝(空回执)")
check(int(inv.inventory_count()) == 2, "Used 保留只读(库存计数不变)")
Kit.check_quiet(inv_proj.size() == 2 and not bool(inv_proj[0].used), "库存投影两件未用")
Kit.check_quiet(bool(inv.consume(int(r1.handle)).get("used", false)), "consume 返回已用快照回执")
Kit.check_quiet(inv.consume(int(r1.handle)).is_empty(), "Used 不可逆——二次 consume 拒绝(空回执)")
Kit.check_quiet(int(inv.inventory_count()) == 2, "Used 保留只读(库存计数不变)")
# ② 真视恒可用+边缘窗口∩板(3×3 中心贴角——角 4 格)
var v_ok: Dictionary = inv.validate_use(int(r2.handle), Vector2i(0, 0), Vector2i(10, 10), Callable())
check(bool(v_ok.ok) and v_ok.resolved.size() == 4,
Kit.check_quiet(bool(v_ok.ok) and v_ok.resolved.size() == 4,
"真视边缘窗口=窗口∩板(角 4 格——护栏只缩不扩)")
# 预读点击级零信息:中心板内但回调恒 false(全已开)→拒绝零消耗
var inv2 = _new_inv()
var rp: Dictionary = inv2.add_item("loot_compass", "safe_cell", Vector2i(1, 1), 1)
var v_zero: Dictionary = inv2.validate_use(int(rp.handle), Vector2i(5, 5), Vector2i(10, 10), Callable())
check(not bool(v_zero.ok) and int(inv2.inventory_count()) == 1,
Kit.check_quiet(not bool(v_zero.ok) and int(inv2.inventory_count()) == 1,
"预读点击级零信息拒绝(不消耗——库存不变)")
# 目标越板
var v_oob: Dictionary = inv2.validate_use(int(rp.handle), Vector2i(12, 5), Vector2i(10, 10), Callable())
check(not bool(v_zero.ok) and not bool(v_oob.ok), "越板目标拒绝")
Kit.check_quiet(not bool(v_zero.ok) and not bool(v_oob.ok), "越板目标拒绝")
# ③ 叠加层+三清(retire 不动库存;reset 全清)
inv2.apply_overlay("mine_reveal", v_ok.resolved)
inv2.apply_overlay("reward_preview", [Vector2i(5, 5)])
var ov: Dictionary = inv2.query_overlays()
check(ov.mine_reveal.size() == 4 and ov.reward_preview.size() == 1, "叠加层两集落位(真视 4/预读 1)")
Kit.check_quiet(ov.mine_reveal.size() == 4 and ov.reward_preview.size() == 1, "叠加层两集落位(真视 4/预读 1)")
inv2.retire_overlays()
check(inv2.query_overlays().mine_reveal.is_empty() and int(inv2.inventory_count()) == 1,
Kit.check_quiet(inv2.query_overlays().mine_reveal.is_empty() and int(inv2.inventory_count()) == 1,
"换图三清:叠加层消亡+库存不动(关卡域)")
inv2.reset()
check(inv2.inventory_count() == 0 and inv2.probe().used == 0, "关卡末 reset 全清(幂等)")
Kit.check_quiet(inv2.inventory_count() == 0 and inv2.probe().used == 0, "关卡末 reset 全清(幂等)")
# ④ 零随机重放:同操作序列两遍,投影逐位相同
var replay_a: Array = []
@@ -106,13 +100,7 @@ func _init() -> void:
replay_a = [inv3.query_inventory(Callable()), inv3.query_overlays(), inv3.probe()]
else:
replay_b = [inv3.query_inventory(Callable()), inv3.query_overlays(), inv3.probe()]
check(str(replay_a) == str(replay_b), "零随机重放:库存+叠加层+探针投影逐位相同")
Kit.check_quiet(str(replay_a) == str(replay_b), "零随机重放:库存+叠加层+探针投影逐位相同")
# ---- 汇总 ----
if _problems.is_empty():
print("[aux_items_test] PASS — 授予归因/Used 不可逆/真视恒可用/零信息拒绝/边缘窗口/三清/零随机重放 全过")
quit(0)
else:
for p in _problems:
push_error("[aux_items_test] FAIL — " + p)
quit(1)
quit(Kit.finish("aux_items_test", "PASS — 授予归因/Used 不可逆/真视恒可用/零信息拒绝/边缘窗口/三清/零随机重放 全过"))
+42 -54
View File
@@ -8,16 +8,11 @@ extends SceneTree
const SaveManagerScript := preload("res://core/save_manager.gd")
const AdventureRunnerScript := preload("res://core/adventure_runner.gd")
const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:check/汇总退出样板单点(静默收集通道)
var _problems: Array = []
var _bp_path := "user://test_breakpoint_resume.json"
func check(cond: bool, msg: String) -> void:
if not cond:
_problems.append(msg)
func _init() -> void:
var db = load("res://core/config_db.gd").new()
if not db.load_tables("res://data/tables", false):
@@ -32,64 +27,64 @@ func _init() -> void:
var adr_a = AdventureRunnerScript.new(db, rng_a)
adr_a.set_save_manager(sm_a)
var params := {"hero_override": {}, "aux_unlocked": {}}
check(adr_a.start_adventure(1234, params), "冒险启动(入口停点)")
check(sm_a.has_breakpoint(), "入口停点断点在档")
check(int((sm_a.breakpoint_snapshot() as Dictionary).get("level_index", -1)) == 1, "断点关卡序号 = 1")
check(adr_a.abandon(), "入口停点放弃受理(非战斗期)")
check(not sm_a.has_breakpoint() and sm_a.write_to_disk(), "放弃清断点+写盘(退出=清除断点)")
Kit.check_quiet(adr_a.start_adventure(1234, params), "冒险启动(入口停点)")
Kit.check_quiet(sm_a.has_breakpoint(), "入口停点断点在档")
Kit.check_quiet(int((sm_a.breakpoint_snapshot() as Dictionary).get("level_index", -1)) == 1, "断点关卡序号 = 1")
Kit.check_quiet(adr_a.abandon(), "入口停点放弃受理(非战斗期)")
Kit.check_quiet(not sm_a.has_breakpoint() and sm_a.write_to_disk(), "放弃清断点+写盘(退出=清除断点)")
var sm_reload = SaveManagerScript.new(_bp_path)
sm_reload.load_from_disk()
check(not sm_reload.has_breakpoint(), "放弃后重读档无断点(「继续上局」隐藏口径)")
Kit.check_quiet(not sm_reload.has_breakpoint(), "放弃后重读档无断点(「继续上局」隐藏口径)")
# ---- Part 2:中断基线——入口停点后选路进战,记板指纹与计数器 ----
var rng_b = load("res://core/rng_service.gd").new()
var sm_b = SaveManagerScript.new(_bp_path)
var adr_b = AdventureRunnerScript.new(db, rng_b)
adr_b.set_save_manager(sm_b)
check(adr_b.start_adventure(1234, params), "基线冒险启动(同种子 1234)")
Kit.check_quiet(adr_b.start_adventure(1234, params), "基线冒险启动(同种子 1234)")
var runner_b = adr_b.runner()
var c0: int = rng_b.counter()
var cand_b: Array = runner_b.next_candidate_ids()
var hp0: int = runner_b.hero_hp
var bp_disk: Dictionary = sm_b.breakpoint_snapshot()
check(int(bp_disk.get("rng_counter", -1)) == c0, "断点计数器 = 当前取数号位(快照契约)")
check(runner_b.pick_next_node(int(cand_b[0])), "基线选路进首战")
Kit.check_quiet(int(bp_disk.get("rng_counter", -1)) == c0, "断点计数器 = 当前取数号位(快照契约)")
Kit.check_quiet(runner_b.pick_next_node(int(cand_b[0])), "基线选路进首战")
var battle_b = runner_b.current_battle()
check(battle_b != null, "基线首战开打")
Kit.check_quiet(battle_b != null, "基线首战开打")
var fp_b := _board_fingerprint(battle_b.minesweeper.current_board())
var c1: int = rng_b.counter()
# ---- Part 3:续玩确定性——新进程对象自盘上断点重建,同种子续玩板逐位一致 ----
var sm_c = SaveManagerScript.new(_bp_path)
check(sm_c.load_from_disk() and sm_c.has_breakpoint(), "断点自盘读回在档")
Kit.check_quiet(sm_c.load_from_disk() and sm_c.has_breakpoint(), "断点自盘读回在档")
var bp_c: Dictionary = sm_c.breakpoint_snapshot()
var rng_c = load("res://core/rng_service.gd").new()
var adr_c = AdventureRunnerScript.new(db, rng_c)
check(adr_c.resume_adventure(bp_c, params), "续玩受理")
Kit.check_quiet(adr_c.resume_adventure(bp_c, params), "续玩受理")
var runner_c = adr_c.runner()
check(int(adr_c.master_seed) == 1234, "续玩主种子回填")
check(int(rng_c.counter()) == c0, "续玩取数号位 = 断点号位(零重耗零跳号)")
check(runner_c.hero_hp == hp0, "续玩生命账本回填")
Kit.check_quiet(int(adr_c.master_seed) == 1234, "续玩主种子回填")
Kit.check_quiet(int(rng_c.counter()) == c0, "续玩取数号位 = 断点号位(零重耗零跳号)")
Kit.check_quiet(runner_c.hero_hp == hp0, "续玩生命账本回填")
var cand_c: Array = runner_c.next_candidate_ids()
check(cand_c.size() == cand_b.size() and int(cand_c[0]) == int(cand_b[0]), "续玩候选一致(停点重建)")
check(runner_c.awaiting_choice(), "续玩处选路停点态")
check(runner_c.pick_next_node(int(cand_c[0])), "续玩选路进首战")
Kit.check_quiet(cand_c.size() == cand_b.size() and int(cand_c[0]) == int(cand_b[0]), "续玩候选一致(停点重建)")
Kit.check_quiet(runner_c.awaiting_choice(), "续玩处选路停点态")
Kit.check_quiet(runner_c.pick_next_node(int(cand_c[0])), "续玩选路进首战")
var battle_c = runner_c.current_battle()
var fp_c := _board_fingerprint(battle_c.minesweeper.current_board())
check(fp_c == fp_b, "续玩首战棋盘与中断前时间线逐位一致(同种子同板)")
check(int(rng_c.counter()) == c1, "续玩开板后号位与基线一致")
Kit.check_quiet(fp_c == fp_b, "续玩首战棋盘与中断前时间线逐位一致(同种子同板)")
Kit.check_quiet(int(rng_c.counter()) == c1, "续玩开板后号位与基线一致")
# ---- Part 4:续玩幂等——同一断点二次续玩(新进程对象)结果逐位同 ----
var sm_d = SaveManagerScript.new(_bp_path)
sm_d.load_from_disk()
var rng_d = load("res://core/rng_service.gd").new()
var adr_d = AdventureRunnerScript.new(db, rng_d)
check(adr_d.resume_adventure(sm_d.breakpoint_snapshot(), params), "二次续玩受理")
Kit.check_quiet(adr_d.resume_adventure(sm_d.breakpoint_snapshot(), params), "二次续玩受理")
var runner_d = adr_d.runner()
check(int(rng_d.counter()) == c0 and runner_d.hero_hp == hp0, "二次续玩状态逐位同(幂等)")
check(runner_d.pick_next_node(int(runner_d.next_candidate_ids()[0])), "二次续玩选路")
Kit.check_quiet(int(rng_d.counter()) == c0 and runner_d.hero_hp == hp0, "二次续玩状态逐位同(幂等)")
Kit.check_quiet(runner_d.pick_next_node(int(runner_d.next_candidate_ids()[0])), "二次续玩选路")
var battle_d = runner_d.current_battle()
check(_board_fingerprint(battle_d.minesweeper.current_board()) == fp_b, "二次续玩板逐位同(幂等)")
Kit.check_quiet(_board_fingerprint(battle_d.minesweeper.current_board()) == fp_b, "二次续玩板逐位同(幂等)")
# ---- Part 5:三选一停点中断→续玩(迭代 10 奖励重构——候选不重抽、已兑付不重付、次数计数一致)----
var rng_e = load("res://core/rng_service.gd").new()
@@ -97,50 +92,43 @@ func _init() -> void:
var adr_e = AdventureRunnerScript.new(db, rng_e)
adr_e.set_save_manager(sm_e)
var params_e := {"hero_override": {}, "aux_unlocked": {"blast_kit": true}}
check(adr_e.start_adventure(4321, params_e), "P5 冒险启动(seed 4321)")
Kit.check_quiet(adr_e.start_adventure(4321, params_e), "P5 冒险启动(seed 4321)")
adr_e._bonus_taken["attack"] = 1 # 白盒预置计数(断点附随件往返的判据面——计数不可由加算区反推)
adr_e._on_level_finished({"result": "victory", "hp": 30, "node_index": 1, "node_id": 1, "node_type": "shop"})
var cands_e: Array = (adr_e._bonus_pending.get("candidates", []) as Array).duplicate()
check(not cands_e.is_empty(), "P5 非末关通关三选一停点在案(直调 victory——L1 非末关)")
Kit.check_quiet(not cands_e.is_empty(), "P5 非末关通关三选一停点在案(直调 victory——L1 非末关)")
var sm_f = SaveManagerScript.new(_bp_path)
check(sm_f.load_from_disk() and sm_f.has_breakpoint(), "P5 选择停点断点在档(选择停点可中断契约)")
Kit.check_quiet(sm_f.load_from_disk() and sm_f.has_breakpoint(), "P5 选择停点断点在档(选择停点可中断契约)")
var bp_f: Dictionary = sm_f.breakpoint_snapshot()
var bp_f_cands: Array = (bp_f.get("bonus_pending", {}) as Dictionary).get("candidates", []) as Array
check(bp_f_cands == cands_e, "P5 断点附随件含候选(bonus_pending 往返)")
check(int((bp_f.get("bonus_taken", {}) as Dictionary).get("attack", -1)) == 1, "P5 断点附随件含次数计数(bonus_taken 往返)")
Kit.check_quiet(bp_f_cands == cands_e, "P5 断点附随件含候选(bonus_pending 往返)")
Kit.check_quiet(int((bp_f.get("bonus_taken", {}) as Dictionary).get("attack", -1)) == 1, "P5 断点附随件含次数计数(bonus_taken 往返)")
var got_cands := {"cands": [] as Array}
var adr_f = AdventureRunnerScript.new(db, load("res://core/rng_service.gd").new())
adr_f.set_save_manager(sm_f) # 兑付后停点快照落盘承载(与真机组合根同构)
adr_f.bonus_choice_presented.connect(func(p): got_cands["cands"] = (p.get("candidates", []) as Array).duplicate())
check(adr_f.resume_adventure(bp_f, params_e), "P5 续玩受理(选择态分支)")
check(adr_f.runner() == null, "P5 续玩不重跑关卡(三选一停点直回,_start_current_level 未走)")
check(got_cands["cands"] == cands_e, "P5 续玩候选不重抽(逐位一致)")
check(int(adr_f._bonus_taken["attack"]) == 1, "P5 次数计数恢复一致(牌库剔除态续接)")
check(not adr_f.continue_to_next_level(), "P5 未兑付推进拒绝(续玩态零自动推进同契约)")
check(adr_f.pick_bonus(String(cands_e[0])), "P5 点选兑付受理")
Kit.check_quiet(adr_f.resume_adventure(bp_f, params_e), "P5 续玩受理(选择态分支)")
Kit.check_quiet(adr_f.runner() == null, "P5 续玩不重跑关卡(三选一停点直回,_start_current_level 未走)")
Kit.check_quiet(got_cands["cands"] == cands_e, "P5 续玩候选不重抽(逐位一致)")
Kit.check_quiet(int(adr_f._bonus_taken["attack"]) == 1, "P5 次数计数恢复一致(牌库剔除态续接)")
Kit.check_quiet(not adr_f.continue_to_next_level(), "P5 未兑付推进拒绝(续玩态零自动推进同契约)")
Kit.check_quiet(adr_f.pick_bonus(String(cands_e[0])), "P5 点选兑付受理")
var picked_e: String = String(adr_f._bonus_result.get("picked", ""))
if adr_f._bonus_taken.has(picked_e):
check(int(adr_f._bonus_taken[picked_e]) == (2 if picked_e == "attack" else 1),
Kit.check_quiet(int(adr_f._bonus_taken[picked_e]) == (2 if picked_e == "attack" else 1),
"P5 兑付计数一致(%s——预置 1 + 本次兑付)" % picked_e)
check(sm_f.has_breakpoint(), "P5 兑付后停点快照在档(已兑付待推进态可中断)")
Kit.check_quiet(sm_f.has_breakpoint(), "P5 兑付后停点快照在档(已兑付待推进态可中断)")
var bp_g: Dictionary = sm_f.breakpoint_snapshot()
var adr_g = AdventureRunnerScript.new(db, load("res://core/rng_service.gd").new())
adr_g.set_save_manager(sm_f) # 已兑付态续玩的后续快照同承载
var got_result := {"r": {}}
adr_g.bonus_resolved.connect(func(r): got_result["r"] = r.duplicate())
check(adr_g.resume_adventure(bp_g, params_e), "P5 已兑付态续玩受理")
check(String(got_result["r"].get("picked", "")) == picked_e, "P5 已兑付不重付(resolved 重发同件)")
check(adr_g.continue_to_next_level(), "P5 已兑付续玩后推进受理(下一关开始)")
Kit.check_quiet(adr_g.resume_adventure(bp_g, params_e), "P5 已兑付态续玩受理")
Kit.check_quiet(String(got_result["r"].get("picked", "")) == picked_e, "P5 已兑付不重付(resolved 重发同件)")
Kit.check_quiet(adr_g.continue_to_next_level(), "P5 已兑付续玩后推进受理(下一关开始)")
_clear_disk()
if _problems.is_empty():
print("[breakpoint_resume_test] PASS — 断点往返/续玩确定性/幂等/退出清断点 全绿")
quit(0)
else:
for p in _problems:
push_error("[breakpoint_resume_test] FAIL — " + p)
print("[breakpoint_resume_test] FAIL — " + p)
quit(1)
quit(Kit.finish("breakpoint_resume_test", "PASS — 断点往返/续玩确定性/幂等/退出清断点 全绿"))
func _board_fingerprint(b) -> String: