Files
gplan-2/game/smoke/adventure_monte_carlo.gd
T
DKChan 3333187351 feat(close): 迭代20 T5+T8+T9+T10 C段收尾(专长实装批)
T5 Boss 兑付面换代:通关渠道三选一改按档配比加权抽取(无放回加权抽 boss_feat_weight
100/45/15/5 provisional 归调参批——奇物.md v3.1 rarity 语义约束兑付面);信号注释换代
「提升三选一」→「专长三选一」(V11 正名);V12 频带断言(300 停点档位频率出带±15pp)。

T8 MC 对账适配:formed 画像 14 旧件→五事件族 14 新件(开格 4/插旗 3/踩雷 4/节奏 2/战斗 1);
新基线取证:normal/off 100 局 71000 档 通关 0%(master 对照同为 0%=零配基线本相),
止步分布 90/10→93/7 可归因曝光槽候选构成;同种子重放 30 局逐位复现 ✓(V15)。

T9 迁出收尾:treasure_items 删 venom_edge/flag_burst 两行(同调目录收窄);schema 词面
venom_rider/flag_burst 摘除(EFFECT_KINDS/MAGNITUDE_KINDS);rewrite_board 效果类型除名
(净化工兵退役——V14 连带清理闭环);V13 断言入 smoke。

T10 空配置验收闸:三选一池空 skip_empty_curio 消化+非空拒绝反例+商店空货架合法态断言。

测试:pool_case_d 17 checks(+V12/T10)/feat_vouchers 23 checks(+V16 断点往返);
冒烟 864/0+run_all 21 套全绿。
2026-10-12 00:28:22 +08:00

153 lines
8.0 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
# Date: 2026-09-21
# 冒险全流程期望扫描(三关连探零加成基线批量试玩——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://smoke/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]
# 同调分层开关(迭代 11 第①批 T8——off=现行基线零装配零扰动;on=贪心装配画像,确定性同种子可复放)
var attune_mode := "off"
if args.size() >= 5 and args[4] in ["off", "on"]:
attune_mode = args[4]
# 商店分层开关(迭代 11 第②批 T5——off=legacy 对照缺省零扰动;on=共享池货架面,终局池读数随行)
var shop_mode := "off"
if args.size() >= 6 and args[5] in ["off", "on"]:
shop_mode = args[5]
# 特性/树分层开关(迭代 12 T7/T10——off=基线;formed=成型画像开局注入七流派各 2 件档一成型集
# 〔测量面专用 params.relics_seed——真实局规则改写型特性只走局内随机渠道〕;ab_a/ab_b=Fork posture 两侧
# rule 键集——A/B 面两侧差 <~3pp 验收载体)
var feat_mode := "off"
if args.size() >= 7 and args[6] in ["off", "formed", "ab_a", "ab_b"]:
feat_mode = args[6]
var feat_profile := {}
if feat_mode == "formed":
# 迭代 20 T8 画像换代——旧 14 件随老 27 件退役;新画像=五事件族覆盖 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": ["posture_first_strike"]}
elif feat_mode == "ab_b":
feat_profile = {"mode": "off", "tree_rules": ["posture_bulwark"]}
var db = load("res://core/config_db.gd").new()
if not db.load_tables("res://data/tables", false):
print("表加载失败")
quit(1)
return
var attune_profile := {}
if attune_mode == "on":
# 贪心画像:中间进度计数(通关 3/Boss 击杀 6)经谓词表求值 Slots;装配序 = 表序 passive 优先至
# Slots-1 + 首个 active(slots=3 → [淬毒刃纹, 标中殉爆, 掘进蓄能],provisional 画像归调参批);
# Y = 营地升级 Lv1 表值 1。autoplay 默认不刷——Y 仅注入容量,刷新消费归玩家策略面(钓鱼归玩家)。
var slots: int = db.attunement_growth.slots({"clear_count": 3, "boss_kill_count": 6})
var passives: Array = []
var actives: Array = []
for r in db.treasure_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://smoke/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.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"]),
# 旗经济五字段(迭代14②判式换代批 T1——η=flag_hit÷flag_placed、标空率=flag_miss÷flag_placed
# 派生归分析侧;gain/penalty=结算流水行值合计(原始行值口径,等价换算归判读侧——登记))
"flag_placed": int(st.get("flag_placed", 0)), "flag_hit": int(st.get("flag_hit", 0)),
"flag_miss": int(st.get("flag_miss", 0)),
"flag_hit_gain": int(st.get("flag_hit_gain", 0)),
"flag_miss_penalty": int(st.get("flag_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_templates": st.get("level_templates", []),
"wallet_count": (st["wallet_events"] as Array).size(),
}
if shop_mode == "on":
# pool 面增量字段(迭代 11 第②批 T5——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)