# VERTICAL SLICE - NOT FOR PRODUCTION # 断点基线对拍采集脚本(迭代 39 拆件批 T0 载体,批末随中间产物退役——非长期测试件)。 # 固定种子集驱动完整冒险至各停点(节点入口/选路/三选一在途/兑付后待推进),逐停点 # dump 断点 Dictionary 全键值(直调 _bp_parts 写侧取值面)→ JSON 落盘,供改前/改后 # 逐键逐位比对(bp1:同种子改前改后 27 键逐位一致)。 # 运行:BP_DUMP_OUT= godot --headless --path . --script res://tests/bp_baseline_dump.gd # 缺省输出 user://bp_baseline_dump.json。 # 驱动与 sim/level_autoplay 同构(完美档战斗驱动 + 确定性选件/选路)——零随机流自开, # 同种子逐位可复放;dump 时点由驱动循环状态判定,确定性时点。 extends SceneTree const AdventureRunnerScript := preload("res://core/adventure_runner.gd") const Autoplay := preload("res://sim/level_autoplay.gd") const SEEDS: Array[int] = [1234, 4321, 910042, 20260101, 20260102, 20260103] const PARAMS := { "hero_override": {}, "treasure_unlocked": {"wide_scout_band": true}, "attunement": ["sky_eye", "incense_stick", "refresh_practice"], # 充能账非零形态+刷新存量加仓(attunement_charges/refresh_stock 键覆盖) "attune_refresh_y": 2, # Y 池非零形态(attune_refresh_y 键覆盖) "shop_mode": "pool", # 共享池货架面(shop_shelf/shop_visit_buys/shop_purchased 覆盖) "relics_seed": ["twin_pick"], # 特性持有集+adventure_counters 非空形态(adv_twin_digs 计数器用户) } func _init() -> void: var db = load("res://core/config_db.gd").new() if not db.load_tables("res://data/tables", false): print("[bp_baseline_dump] FAIL — 数值表加载失败") quit(1) return var runs: Array = [] for seed_val in SEEDS: var run := _drive_dump_run(db, int(seed_val)) runs.append({"seed": int(seed_val), "result": String(run["result"]), "dumps": run["dumps"]}) print("[bp_baseline_dump] seed=%d result=%s dumps=%d" % [int(seed_val), String(run["result"]), (run["dumps"] as Array).size()]) var out_path := OS.get_environment("BP_DUMP_OUT") if out_path.is_empty(): out_path = "user://bp_baseline_dump.json" var f := FileAccess.open(out_path, FileAccess.WRITE) if f == null: print("[bp_baseline_dump] FAIL — 输出文件打不开: %s" % out_path) quit(1) return f.store_string(JSON.stringify({"generated_by": "bp_baseline_dump", "seeds": SEEDS, "runs": runs}, " ")) f.close() print("[bp_baseline_dump] DONE — %s" % out_path) quit(0) # 单局驱动+停点 dump(返回 {result, dumps};dumps 元素 = {seq, tag, level_index, node_id, bp}) func _drive_dump_run(db, seed_val: int) -> Dictionary: var rng = load("res://core/rng_service.gd").new() rng.set_master_seed(seed_val) var adr = AdventureRunnerScript.new(db, rng) var ctx := {"result": "", "dumps": [] as Array, "seq": 0, "adr": adr, "db": db} # 信号直连(拆件批 T3——转发族退役):runner 信号在 level_started 重取点重连(同关守卫恰一次); # 停点件信号面订 stop()(每冒险一次) adr.level_started.connect(_on_dump_level_started.bind(ctx)) adr.stop().curio_resolved.connect(_on_curio_resolved.bind(ctx)) if not adr.start_adventure(seed_val, PARAMS): ctx["result"] = "start_failed" return ctx var guard := 2000 while String(ctx["result"]) == "" and guard > 0: guard -= 1 if not (adr.stop()._node_pending as Dictionary).is_empty(): _dump(ctx, "node_curio_pending") var ncands: Array = (adr.stop()._node_pending.get("candidates", []) as Array) var npick: Dictionary = Autoplay._pick_token(ncands, "off") if not adr.stop().apply_node_curio(String(npick.get("kind", "")), String(npick.get("tier", "")), String(npick.get("feat_id", ""))): ctx["result"] = "node_advance_failed" continue if not (adr.stop()._pending as Dictionary).is_empty(): _dump(ctx, "curio_pending") var cands: Array = (adr.stop()._pending.get("candidates", []) as Array) var pick: Dictionary = Autoplay._pick_token(cands, "off") if not adr.stop().pick_curio(String(pick.get("kind", "")), String(pick.get("tier", "")), String(pick.get("feat_id", ""))): ctx["result"] = "advance_failed" continue if bool(adr._pending_next_level): _dump(ctx, "resolved_wait_continue") if not adr.continue_to_next_level(): ctx["result"] = "advance_failed" continue var runner = adr.runner() if runner == null: break # 冒险已收束或停点态未覆盖(防御) if runner.awaiting_choice(): _dump(ctx, "route_choice") var ids: Array = runner.next_candidate_ids() if ids.is_empty(): ctx["result"] = "no_candidates" break if not runner.pick_next_node(int(ids[0])): ctx["result"] = "route_failed" continue var settlement = runner.current_battle() if settlement != null and String(settlement.battle_result) == "": _drive_battle(ctx, runner, settlement) continue # 收束态分派:结算受理后(state=idle/settling)按 summary 定局;其余态无战无停点无选路 = 死循环防护 if String(ctx["result"]) == "": var sum: Dictionary = adr.summary if adr.summary != null else {} if not sum.is_empty(): ctx["result"] = String(sum.get("result", "settled")) break if String(adr.state) in ["idle", "settling"]: ctx["result"] = "settled_no_summary" break if guard == 1: ctx["result"] = "timeout" return ctx # 完美档战斗驱动(复用 sim 同款决策——上界策略;st 键集满足 _autoplay_battle 消费面) func _drive_battle(ctx: Dictionary, runner, settlement) -> void: var st := {"result": "", "rounds": 0, "refreshes": 0, "seal_placed": 0, "policy": "perfect", "adv": false} Autoplay._autoplay_battle(runner, settlement, st, null) # 停点 dump(写侧取值面直调;_runner 为 null 的停点态节点域字段按缺省落档——与真机同构) static func _dump(ctx: Dictionary, tag: String) -> void: var adr = ctx["adr"] var bp: Dictionary = adr._bp_parts() var runner = adr.runner() var dumps: Array = ctx["dumps"] ctx["seq"] = int(ctx["seq"]) + 1 dumps.append({ "seq": int(ctx["seq"]), "tag": String(tag), "level_index": int(adr.level_index), "node_id": int(runner.current_node_id()) if runner != null else -1, "bp": bp, }) # 每关开始(dump 直连面):重取 runner 重连 node_entered(同关守卫——level_started 每关恰一次发射) static func _on_dump_level_started(_li: int, _dn: String, ctx: Dictionary) -> void: var adr = ctx["adr"] if String(ctx.get("wired_level", "")) == "%s|%d" % [str(adr.master_seed), int(adr.level_index)]: return ctx["wired_level"] = "%s|%d" % [str(adr.master_seed), int(adr.level_index)] var r = adr.runner() if r == null: return r.node_entered.connect(_on_node_entered.bind(ctx)) # 节点入口驱动(信号时点):入口 dump 覆盖节点域字段形态;rest/shop 直通继续,reward 呈现三选一 static func _on_node_entered(node: Dictionary, _info: Dictionary, ctx: Dictionary) -> void: var adr = ctx["adr"] var runner = adr.runner() if runner == null: return var t := String(node.get("type", "")) _dump(ctx, "node_" + t) if t == "rest" or t == "shop": runner.continue_after_node() elif t == "reward": var ndraw: Dictionary = adr.stop().node_curio_candidates() var ncands: Array = ndraw.get("candidates", []) as Array if ncands.is_empty(): runner.continue_after_node() # 牌库空合法态直通(池耗即枯竭) else: adr.stop().present_node_curio(ncands) # 节点三选一兑付完成回图(context=reward_node → continue_after_node 回选路) static func _on_curio_resolved(result: Dictionary, ctx: Dictionary) -> void: if String(result.get("context", "")) != "reward_node": return var adr = ctx["adr"] var runner = adr.runner() if runner != null and not runner.continue_after_node(): ctx["result"] = "node_advance_failed"