Files
gplan-2/game/tests/bp_baseline_dump.gd
T

178 lines
7.8 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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"