174 lines
8.8 KiB
GDScript
174 lines
8.8 KiB
GDScript
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||
# 冒险全流程期望扫描(三关连探零加成基线批量试玩——run_adventure 的蒙特卡洛批量工具,
|
||
# 补 level_monte_carlo 只测单关的空白面):固定 atk+0%/hp+0%(零天赋格加成,关间大奖励
|
||
# 按冒险结构现行设计自动兑付),逐局独立种子,输出:
|
||
# ① 每局一行 RUNSUM JSON(stdout,局级摘要:策略/种子/result/summary/战斗轮数/承伤/踩雷/
|
||
# 刷新/行进序列/逐战上限序列——后续统计分析的取数面);
|
||
# ② 全量操作日志 = playtest_logger 共享会话 JSONL(与真人试玩同合规口径,每策略一批一文件,
|
||
# run 行携带种子/策略切分局次,落 user://logs/)。
|
||
# 本工具只采集与落盘数据,不做统计判读(关卡完成率/败因分布归分析会话)。
|
||
# 运行:Godot_console.exe --headless --path . --script res://sim/adventure_monte_carlo.gd -- [N] [seed0] [normal|perfect|both] [route min|max]
|
||
extends SceneTree
|
||
|
||
|
||
var _run_cursor := 0 # 全局局序号(seed = seed0 + cursor,逐局独立消费一次)
|
||
|
||
|
||
func _initialize() -> void:
|
||
var args := OS.get_cmdline_user_args()
|
||
var n := 200
|
||
if args.size() >= 1 and args[0].is_valid_int():
|
||
n = maxi(5, int(args[0]))
|
||
var seed0 := 71000
|
||
if args.size() >= 2 and args[1].is_valid_int():
|
||
seed0 = int(args[1])
|
||
var policy_arg := "both"
|
||
if args.size() >= 3 and args[2] in ["normal", "perfect", "both"]:
|
||
policy_arg = args[2]
|
||
var policies: Array = ["normal"]
|
||
if policy_arg == "perfect":
|
||
policies = ["perfect"]
|
||
elif policy_arg == "both":
|
||
policies = ["normal", "perfect"]
|
||
var route := "min"
|
||
if args.size() >= 4 and args[3] in ["min", "max"]:
|
||
route = args[3]
|
||
# 同调分层开关(off=现行基线零装配零扰动;on=贪心装配画像,确定性同种子可复放)
|
||
var attune_mode := "off"
|
||
if args.size() >= 5 and args[4] in ["off", "on"]:
|
||
attune_mode = args[4]
|
||
# 商店分层开关(off=legacy 对照缺省零扰动;on=共享池货架面,终局池读数随行)
|
||
var shop_mode := "off"
|
||
if args.size() >= 6 and args[5] in ["off", "on"]:
|
||
shop_mode = args[5]
|
||
# 特性/树分层开关(off=基线;formed=成型画像开局注入五事件族 14 件成型集
|
||
# 〔测量面专用 params.relics_seed——真实局规则改写型特性只走局内随机渠道〕;ab_a/ab_b=Fork posture 两侧
|
||
# rule 键集——A/B 面两侧差 <~3pp 验收载体;rules=任意树规则键集注入〔逗号分隔 args[7]——V19 三组配对载体〕)
|
||
var feat_mode := "off"
|
||
if args.size() >= 7 and args[6] in ["off", "formed", "ab_a", "ab_b", "rules"]:
|
||
feat_mode = args[6]
|
||
var db = load("res://core/config_db.gd").new()
|
||
if not db.load_tables("res://data/tables", false):
|
||
print("表加载失败")
|
||
quit(1)
|
||
return
|
||
var feat_profile := {}
|
||
if feat_mode == "formed":
|
||
# 成型画像=五事件族覆盖 14 件
|
||
# (开格 4/封印 3/踩雷 4/节奏 2/战斗 1——每族含跨事件账本件,账本件=攒层/计数面 MC 读数承载)
|
||
feat_profile = {"mode": "on", "seed_ids": [
|
||
"twin_pick", "geologist", "hard_hat", "frontier_miner",
|
||
"seal_echo", "seal_smith", "seal_stake",
|
||
"pain_memory", "trap_instinct", "chain_traps", "blood_honey",
|
||
"last_drop", "market_close",
|
||
"execution_scholar"]}
|
||
elif feat_mode == "ab_a":
|
||
feat_profile = {"mode": "off", "tree_rules": _tree_rule_dict(db, ["posture_first_strike"])}
|
||
elif feat_mode == "ab_b":
|
||
feat_profile = {"mode": "off", "tree_rules": _tree_rule_dict(db, ["posture_bulwark"])}
|
||
elif feat_mode == "rules":
|
||
feat_profile = {"mode": "off", "tree_rules": _tree_rule_dict(db, Array(String(args[7] if args.size() >= 8 else "").split(",", false)))} # V19——任意键集配对注入(规则键参数字典形态)
|
||
var attune_profile := {}
|
||
if attune_mode == "on":
|
||
# 贪心画像:Slots=3(槽位判定源=树同调位节点点亮数,模拟画像=三节点
|
||
# 全点亮常量);装配序 =
|
||
# 表序 passive 优先至 Slots-1 + 首个 active(slots=3 → 表序头三件占满示例,
|
||
# provisional 画像归调参);Y = 营地升级 Lv1 表值 1。autoplay 默认不刷——Y 仅注入容量,
|
||
# 刷新消费归玩家策略面(钓鱼归玩家)。
|
||
var slots: int = 3
|
||
var passives: Array = []
|
||
var actives: Array = []
|
||
for r in db.crossrun.treasure_items.attunement_rows():
|
||
if String(r.get("form", "")) == "passive":
|
||
passives.append(String(r.get("item_id", "")))
|
||
elif String(r.get("form", "")) == "active_charged":
|
||
actives.append(String(r.get("item_id", "")))
|
||
var equipped: Array = []
|
||
for p in passives:
|
||
if equipped.size() >= maxi(0, slots - 1):
|
||
break
|
||
equipped.append(p)
|
||
if slots > equipped.size() and not actives.is_empty():
|
||
equipped.append(actives[0])
|
||
for p in passives:
|
||
if equipped.size() >= slots:
|
||
break
|
||
if not equipped.has(p):
|
||
equipped.append(p)
|
||
attune_profile = {"equipped": equipped, "refresh_y": 1}
|
||
var autoplay = load("res://sim/level_autoplay.gd")
|
||
var t0 := Time.get_ticks_msec()
|
||
print("=== 冒险全流程扫描:三关连探 · N=%d/策略 · 种子 %d 起逐局独立 · 策略=%s · atk+0%%/hp+0%% 零加成 · route=%s ===" % [n, seed0, policy_arg, route])
|
||
for policy in policies:
|
||
# 每策略一个共享日志会话(全量 JSONL 体量随局数增长,分文件便于定位)
|
||
var logger = load("res://presentation/playtest_logger.gd").new()
|
||
logger.ap_per_round = int(db.run.action_economy.ap_per_round)
|
||
var log_path: String = logger.start_session(seed0)
|
||
var tally := {}
|
||
for i in range(n):
|
||
var seed_val := seed0 + _run_cursor
|
||
_run_cursor += 1
|
||
var st = autoplay.run_adventure(db, seed_val, 0, 0, false, logger, policy, route, {}, attune_profile,
|
||
{"mode": "pool"} if shop_mode == "on" else {}, feat_profile)
|
||
var sum: Dictionary = st["summary"]
|
||
var rec := {
|
||
"policy": policy, "seed": seed_val, "atk_pct": 0, "hp_pct": 0, "route": route,
|
||
"attune": attune_mode, "equipped": attune_profile.get("equipped", []),
|
||
"feat_mode": feat_mode,
|
||
"result": st["result"],
|
||
"cleared": bool(sum.get("cleared", false)),
|
||
"levels_cleared": int(sum.get("levels_cleared", -1)),
|
||
"stopped_at": int(sum.get("stopped_at", -1)),
|
||
"keystone_total": int(sum.get("keystone_total", -1)),
|
||
"battles": int(st["battles"]), "rounds": int(st["rounds"]),
|
||
"hero_dmg": int(st["hero_dmg"]), "mine_hits": int(st["mine_hits"]),
|
||
"mine_dmg": int(st.get("mine_dmg", 0)),
|
||
"taken": int(st["hero_dmg"]) + int(st.get("mine_dmg", 0)),
|
||
"refreshes": int(st["refreshes"]), "choices": int(st["choices"]),
|
||
# 封印经济五字段(η=seal_hit÷seal_placed、标空率=seal_miss÷seal_placed
|
||
# 派生归分析侧;gain/penalty=结算流水行值合计(原始行值口径,等价换算归判读侧——登记))
|
||
"seal_placed": int(st.get("seal_placed", 0)), "seal_hit": int(st.get("seal_hit", 0)),
|
||
"seal_miss": int(st.get("seal_miss", 0)),
|
||
"seal_hit_gain": int(st.get("seal_hit_gain", 0)),
|
||
"seal_miss_penalty": int(st.get("seal_miss_penalty", 0)),
|
||
"relics": st.get("relics", []),
|
||
"visited_count": (st["visited_ids"] as Array).size(),
|
||
"encounter_max_hp_seq": st["encounter_max_hp_seq"],
|
||
"reward_events": st["reward_events"],
|
||
"shop_gp_seq": st["shop_gp_seq"],
|
||
"level_maps": st.get("level_maps", []),
|
||
"wallet_count": (st["wallet_events"] as Array).size(),
|
||
}
|
||
if shop_mode == "on":
|
||
# pool 面增量字段(off 行零新增保对拍逐位干净)
|
||
rec["shop_mode"] = "pool"
|
||
rec["shop_buys"] = int(st.get("shop_buys", 0))
|
||
rec["pool_stock"] = st.get("pool_stock", {})
|
||
var rs := String(sum.get("result", ""))
|
||
tally[rs] = int(tally.get(rs, 0)) + 1
|
||
print("RUNSUM " + JSON.stringify(rec))
|
||
if (i + 1) % 20 == 0:
|
||
print("... %s 进度 %d/%d(耗时 %.1f 秒)" % [policy, i + 1, n, float(Time.get_ticks_msec() - t0) / 1000.0])
|
||
logger.close_session()
|
||
print("BATCH %s N=%d 终局计数=%s 日志=%s" % [policy, n, JSON.stringify(tally), log_path])
|
||
print("(口径注:零加成 = atk/hp 天赋格百分比均 0;关间大奖励(HP 上限提升+回满)按冒险结构设计自动兑付,未额外剥离 · 全程日志见 user://logs/ · 总耗时 %.1f 秒)" % [float(Time.get_ticks_msec() - t0) / 1000.0])
|
||
quit(0)
|
||
|
||
|
||
## 树规则参数字典构造(A/B 面注入单点——扫 meta_tree Fork 行 effects_a/b,命中 keys 集的键取其
|
||
## 参数字典原样透传;db 参鸭子型沿 board_generator 先例——sim 面不在 S9 扫描面)
|
||
static func _tree_rule_dict(db, keys: Array[Variant]) -> Dictionary[Variant, Variant]:
|
||
var out: Dictionary[Variant, Variant] = {}
|
||
var keyset := {}
|
||
for k in keys:
|
||
keyset[String(k)] = true
|
||
for row in db.crossrun.meta_tree.rows:
|
||
if String(row.get("kind", "")) != "fork":
|
||
continue
|
||
for skey in ["effects_a", "effects_b"]:
|
||
var fx: Dictionary = row.get(skey, {}) as Dictionary
|
||
for rk in fx.keys():
|
||
if keyset.has(String(rk)) and not out.has(String(rk)):
|
||
out[String(rk)] = fx[rk]
|
||
return out
|