178 lines
7.8 KiB
GDScript
178 lines
7.8 KiB
GDScript
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||
# 断点基线对拍采集脚本(迭代 39 拆件批 T0 载体,批末随中间产物退役——非长期测试件)。
|
||
# 固定种子集驱动完整冒险至各停点(节点入口/选路/三选一在途/兑付后待推进),逐停点
|
||
# dump 断点 Dictionary 全键值(直调 _bp_parts 写侧取值面)→ JSON 落盘,供改前/改后
|
||
# 逐键逐位比对(bp1:同种子改前改后 27 键逐位一致)。
|
||
# 运行:BP_DUMP_OUT=<json 路径> 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"
|