# VERTICAL SLICE - NOT FOR PRODUCTION # 日志合规:任何模拟运行与真人试玩同日志——操作记录 / 轮次状态 / 战斗结束 / 关卡完成 全量 JSONL。 # logger=null 时自建会话(每局一文件),批量调用方传共享 logger(log_run 局次标记切分)。 # 关卡自动完成策略(理性玩家近似)——呈现层数值调整板的批量模拟与 headless 扫描 # 共用同一实现,保证"面板模拟读数"与"扫描表读数"同口径。策略与 smoke_all 战斗自动玩家同族: # 挖最近可挖格 / 无可挖停手 / 奖励恢复点立即确认。 # run_level = 机制夹具/单战锚点专用(smoke_all 机制断言、rhythm/defense/damage 单战扫描)—— # 关卡完成率/难度类测量禁用本入口(满血零上下文进关,与真实冒险条件关卡完成率脱节),一律走 run_adventure。 # 策略档 policy:"normal"(默认)/ "perfect"(零盲挖/全封印/卡死刷新——上界测量仪,求解器与 # 动作语义见 auto_player.gd 完美策略档节)。 # 选路驱动(零自动推进):兑付确认/战斗胜利后 runner 进选路态,驱动循环按 route 策略显式 # pick_next_node;route = "min"/"max"(候选节点句柄最小/最大——确定性路径轴,零随机流自开;默认 "min")。 # run_adventure 三关连探驱动(AdventureRunner——主种子恰一次、关卡完成兑付停点确认、adventure_finished # 置 result/summary;外层每关重取 runner 实例经 _drive_level 复用单关驱动)。 # 大奖励停点 = 三选一驱动(候选取首件/替换放弃新件)。 # 普通档 v3 = 完美策略 + 受控失误注入(失误流走独立策略 RNG——种子派生自局种子,逐局确定性/ # 可回放保持,核心七流零消费零扰动;novice_action 留库不删——历史读数对账面)。 # 完美档上界:选路目标价值评分(rest 伤重优先/shop 有钱优先/boss 缓冲)+ 商店贪心买齐。 # route 轴:评分同分 tie-break 仍按 min/max 编号方向——verifier 双窗轴不塌。 # 运行前提:db 已 load_tables;run_level/run_adventure 均须全新 RngService(同种子 ⇒ 全程逐步可回放)。 # 实现注:信号处理体全部为静态方法 + bind 传状态——GDScript 多行 lambda 内嵌 match/控制流的解析不可靠。 extends RefCounted const AutoPlayer := preload("res://sim/auto_player.gd") # 自动玩家策略单一实现 const BattleDriver := preload("res://sim/battle_driver.gd") # 庚案收敛:驱动循环单点(策略仍归 _decide_act 单点分派) # ── 普通档 v3 失误注入参数(踩雷期望=完美+2/板、每板少挖几个正确格)── # 标雷→误挖转化率:主踩雷源=卡死赌挖(不确定性概率命中),误挖只作偶发真手滑—— # 真人几乎不挖自己封印的格(p 过高会让首轮已证封印变保底命中,非失误形态) const NORMAL_MINE_SLIP_P := 0.10 # 安全挖跳过率:每板安全格 ~70-90 × p ≈ 3-5 次漏挖(信息与推进双损失) const NORMAL_SKIP_DIG_P := 0.05 # 空闲提前赌挖率:已证安全格在手时小概率先赌一格——完美系把不确定性攒到残局 # (残局 hazard→1=扫雷),真人早赌滚信息(低 hazard 期吃掉不确定性);p×~40 安全 AP/板 ≈ 4 次 const NORMAL_IDLE_GAMBLE_P := 0.10 static func run_level(db, seed_val: int, level_index: int, atk_pct: int, hp_pct: int, collect_rows: bool = false, logger = null, policy: String = "normal", route: String = "min") -> Dictionary: var own_session := logger == null if own_session: logger = load("res://presentation/playtest_logger.gd").new() logger.ap_per_round = int(db.run.action_economy.ap_per_round) # 注入序前置(守卫拒后开——R5 必填) logger.start_session(seed_val) logger.log_run({"seed": seed_val, "level_index": level_index, "atk_pct": atk_pct, "hp_pct": hp_pct, "policy": policy, "route": route}) var rng = load("res://core/rng_service.gd").new() rng.set_master_seed(seed_val) var runner = load("res://core/level_runner.gd").new(db, rng) var st := { "result": "", "hp_left": 0, "battles": 0, "rounds": 0, "hero_dmg": 0, "rest_healed": 0, "hp_before_rest": -1, "defeat_node_index": -1, "defeat_node_id": -1, "defeat_node_type": "", "boss_depth": -1, "hp_at_boss": -1, "rows": [], "timeout": false, "log_path": "", "policy": policy, "refreshes": 0, "mine_hits": 0, "mine_dmg": 0, "visited_ids": [], "choices": 0, "last_battle_enemies": [], # 封印经济五字段(runsum 读数面;聚合自结算流水 source_class, # 零新随机源零核心改动;η=seal_hit÷seal_placed 与标空率派生归分析侧) "seal_placed": 0, "seal_hit": 0, "seal_miss": 0, "seal_hit_gain": 0, "seal_miss_penalty": 0, "prng": _policy_rng(seed_val), "mine_slips": 0, "skip_digs": 0, "shop_buys": 0, } runner.step_row.connect(_on_step_row.bind(runner, st, collect_rows)) runner.node_entered.connect(_on_node_entered.bind(runner, st, logger)) runner.level_finished.connect(_on_level_finished.bind(st, logger)) runner.set_hero_override(atk_pct, hp_pct) if not runner.start_level(level_index, seed_val, {}): st.result = "start_failed" runner.restore_hero_base() if own_session: logger.close_session() return st _drive_level(runner, st, logger, policy, route) if st.result == "": st.result = "timeout" st.timeout = true for v in runner.visited_ids(): st.visited_ids.append(int(v)) runner.restore_hero_base() st.log_path = logger.log_path() if own_session: logger.close_session() return st # 单关驱动段(run_level 抽取——零自动推进停点 + 战斗自动玩家;policy 经 st 入 _autoplay_battle) # 入口停点(node_index 尚 -1 = 进节点前无 current_node)也经本分支驱动—— # 读 next_candidate_ids 候选(入口单候选)→ pick_next_node,同一 _enter_node 等价(零 RNG 漂移); # 入口 pick 不计 st.choices(单候选强制选路,计数口径与测量面同构)。 static func _drive_level(runner, st: Dictionary, logger, policy: String, route: String) -> void: st["policy"] = policy var guard_battles := 20 var guard_actions := 200 while st.result == "" and guard_battles > 0 and guard_actions > 0: guard_actions -= 1 if runner.awaiting_choice(): # 选路态(入口停点 / 兑付确认 / 战斗胜利后的零自动推进停点)——route 策略显式选下一跳 if runner.node_index >= 0: st.choices = int(st.choices) + 1 runner.pick_next_node(_route_pick(runner, route, st)) continue guard_battles -= 1 var this_settlement = runner.current_battle() if this_settlement == null: break # level_finished 已发(settlement 置 null)而信号尚未置 result 的防御 _autoplay_battle(runner, this_settlement, st, logger) # 三关连探冒险驱动——AdventureRunner 编排:主种子恰一次 # (start_adventure 唯一 new_adventure 调用点)、大奖励立即确认、冒险终局置 result/summary。 # 返回结构 = run_level 同族 + summary / wallet_events / encounter_max_hp_seq / reward_events / # visited_ids(全冒险节点行进序列——跨关累计,非末关 runner 的 visited 随实例更替,此处为唯一全量面)。 # hero_override 尾参 = 永久强化 flat 注入透传(缺省 {} 与既有调用方 # 逐位一致零行为变更)+ run 行携带该参数(分析侧按元进度档切分)+ 败退敌人名采集 # (last_battle_enemies——末场战斗敌方显示名序列,失败局即死亡遭遇)。 static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_rows: bool = false, logger = null, policy: String = "normal", route: String = "min", hero_override: Dictionary = {}, attune: Dictionary = {}, shop: Dictionary = {}, relic: Dictionary = {}) -> Dictionary: var own_session := logger == null if own_session: logger = load("res://presentation/playtest_logger.gd").new() logger.ap_per_round = int(db.run.action_economy.ap_per_round) # 注入序前置(守卫拒后开——R5 必填) logger.start_session(seed_val) logger.log_run({"seed": seed_val, "adventure": true, "atk_pct": atk_pct, "hp_pct": hp_pct, "policy": policy, "route": route, "hero_override": hero_override}) var rng = load("res://core/rng_service.gd").new() rng.set_master_seed(seed_val) var adr = load("res://core/adventure_runner.gd").new(db, rng) var st := { "result": "", "hp_left": 0, "battles": 0, "rounds": 0, "hero_dmg": 0, "rest_healed": 0, "hp_before_rest": -1, "defeat_node_index": -1, "defeat_node_id": -1, "defeat_node_type": "", "boss_depth": -1, "hp_at_boss": -1, "rows": [], "timeout": false, "log_path": "", "policy": policy, "refreshes": 0, "mine_hits": 0, "mine_dmg": 0, "visited_ids": [], "choices": 0, "last_battle_enemies": [], "summary": {}, "wallet_events": [], "encounter_max_hp_seq": [], "reward_events": [], "feat_mode": String(relic.get("mode", "off")), # 特性分层开关(off=316-f 通用竞争/on=成型画像优先捕捞 relic) "shop_gp_seq": [], # 商店房到达余额序(逐关 1 次——GP 跨关延续验收取数面) # 封印经济五字段(与 run_level 同口径,见彼处注释) "seal_placed": 0, "seal_hit": 0, "seal_miss": 0, "seal_hit_gain": 0, "seal_miss_penalty": 0, "prng": _policy_rng(seed_val), "mine_slips": 0, "skip_digs": 0, "shop_buys": 0, "adv": true, # v3 档位行为开关(选路评分/失误注入/商店购买只挂冒险测量面——单关夹具语义不变) "level_maps": [], # 逐关节点图标识(生成图 = 关卡序号+本局主种子——MC 分层出数) } # 信号直连(拆件批 T3):runner 信号在 level_started 重取点重连(转发族退役——重取点前移到本回调) adr.level_started.connect(_on_autoplay_level_started.bind(adr, st, collect_rows, logger)) # 首关信号在 start_adventure 内同步发射(驱动循环起步前)——入口候选订阅须在本行前就位: # _on_autoplay_level_started 于本行同步执行(start_adventure 返回前),runner 三信号先于驱动循环就绪 # 三选一停点驱动(确定性策略候选取首件、 # 兑付后推进下一关——零随机流自开,同种子逐位可复放;无满位替换策略;信号面归停点件) adr.stop().curio_choice_presented.connect(_on_adventure_bonus.bind(st, adr)) adr.stop().curio_resolved.connect(_on_adventure_curio_resolved.bind(st, adr)) adr.adventure_finished.connect(_on_adventure_finished.bind(st, logger)) adr.set_hero_override(atk_pct, hp_pct) # 同调分层注入(缺省 {} 与既有调用方逐位一致零行为变更) var adv_params := {"hero_override": hero_override} if not attune.is_empty(): adv_params["attunement"] = attune.get("equipped", []) adv_params["attune_refresh_y"] = int(attune.get("refresh_y", 0)) # 特性注入(formed 画像=开局注入七流派各 2 件档一成型集;测量面专用) if relic.has("seed_ids"): adv_params["relics_seed"] = relic.get("seed_ids", []) # 树规则参数字典注入(A/B 面读数载体:fork 激活侧 effects_* 参数字典透传) if relic.has("tree_rules"): adv_params["tree_rules"] = relic.get("tree_rules", {}) if relic.get("tree_rules", {}) is Dictionary else {} # 商店分层注入(缺省 legacy 与既有调用方逐位一致零行为变更;pool=共享池货架面) var pool_shop: bool = String(shop.get("mode", "")) == "pool" if pool_shop: adv_params["shop_mode"] = "pool" st["shop_mode"] = "pool" if not adr.start_adventure(seed_val, adv_params): # 测量夹具恒抽签(v0.4——首次脚本分支不进任何读数) st.result = "start_failed" if own_session: logger.close_session() return st var guard_drives := 10 # 每次外层迭代 = 驱动一关(关卡更替由三选一兑付确认与 runner 重取承载) while st.result == "" and guard_drives > 0: guard_drives -= 1 _drive_level(adr.runner(), st, logger, policy, route) if st.result == "": st.result = "timeout" st.timeout = true if adr.runner() != null: adr.runner().restore_hero_base() if pool_shop: # 终局池读数(摘要 additive 键 pool_stock:_settle 清账前快照, # 商店买走 vs 奖励领走 vs 枯竭的终局面;事后读 adr.pool_stock 会被收束清账重置=满库假象) st["pool_stock"] = (st.get("summary", {}).get("pool_stock", {}) as Dictionary).duplicate(true) st["relics"] = (st.get("summary", {}).get("relics", []) as Array).duplicate() # 特性终局持有集(摘要 additive 键清账前快照,同 pool_stock 模式) st.log_path = logger.log_path() if own_session: logger.close_session() return st # 每关开始(autoplay 直连面):重取 runner 并重连三信号(恰好一次——同关守卫: # level_started 每关恰一次发射,bind Callable 每次新建故以关卡号守卫防重入) static func _on_autoplay_level_started(level_index: int, _display_name: String, adr, st: Dictionary, collect_rows: bool, logger) -> void: if int(st.get("wired_level", -1)) == int(level_index): return # 同关重入防御(恰好一次——bind 句柄每回调新建,不以 Callable 相等判重) st["wired_level"] = int(level_index) var r = adr.runner() if r == null: return r.step_row.connect(_on_step_row.bind(adr, st, collect_rows)) r.node_entered.connect(_on_adventure_node_entered.bind(adr, st, logger)) r.wallet_changed.connect(_on_adventure_wallet.bind(st, logger)) # 冒险模式节点采集(_on_node_entered 的 AdventureRunner 门面版——runner 逐关更替,经 adr.runner() 重取; # encounter_max_hp_seq 抓 effective_max_hp:node_entered 先于 begin_battle,encounter 尚为空字典, # 该值 = begin_battle 将收到 hero_max_hp 同一表达式〔规划书抓取点与代码事实冲突,按代码为准已登记〕) static func _on_adventure_node_entered(node: Dictionary, info: Dictionary, adr, st: Dictionary, logger) -> void: var r = adr.runner() var t := String(node.get("type", "")) st.visited_ids.append(int(r.node_id_of(node))) if not st.has("level_maps"): st["level_maps"] = [] st["level_maps"].append("%d#%d" % [int(r.level_row.get("level_index", 0)), int(adr.master_seed)]) # MC 分层出数(关卡×生成图标识) if t == "battle" or t == "boss": st.battles += 1 st.last_battle_enemies = [] # 末场战斗敌方名序列按战斗重置(失败局终值 = 死亡遭遇) st.encounter_max_hp_seq.append(int(r.effective_max_hp())) if logger != null and r.current_battle() != null: logger.attach_battle(r.current_battle()) if t == "boss": st.boss_depth = int(node.get("depth", -1)) st.hp_at_boss = adr.stats().hero_hp elif t == "reward": # 奖励节点三选一(共享池确定性策略;TP0 起 316-f 判读:1 级最稀有、 # 同档并列取最左;替换放弃新件;兑付后 continue_after_node 回图选路) var ndraw: Dictionary = adr.stop().node_curio_candidates() var ncands: Array = ndraw.get("candidates", []) as Array st.reward_events.append({"node_curio_pick": ncands.duplicate(true)}) if ncands.is_empty(): r.continue_after_node() # 牌库空合法态直通(池耗即枯竭——防御保留) else: adr.stop().present_node_curio(ncands) var npick: Dictionary = _pick_token(ncands, String(st.get("feat_mode", "off"))) if adr.stop().apply_node_curio(String(npick.get("kind", "")), String(npick.get("tier", "")), String(npick.get("feat_id", ""))): # 专长凭证拿到即开——候选已被替换为专长三选一,就地再选一轮(确定性策略同款 316-f); # 不作此分支则节点停点悬空→选路卡死(MC timeout 形态) if String(npick.get("kind", "")) == "feat_ticket": var fcands: Array = (adr.stop().node_curio_pending_state().get("candidates", []) as Array) if not fcands.is_empty(): var fpick: Dictionary = _pick_token(fcands, String(st.get("feat_mode", "off"))) adr.stop().apply_node_curio(String(fpick.get("kind", "")), String(fpick.get("tier", "")), String(fpick.get("feat_id", ""))) else: st.result = "advance_failed" elif t == "rest": st.rest_healed = int(info.get("healed", 0)) st.hp_before_rest = int(info.get("hp", 0)) - st.rest_healed r.continue_after_node() elif t == "shop": st.shop_gp_seq.append(int(info.get("gp", -1))) if String(st.get("shop_mode", "legacy")) == "pool": _autoplay_shop_pool(r, st) # 共享池货架贪心(买一件=奖励面少一件的机会成本,画像维持贪心语义) else: _autoplay_shop(r, st) # 贪心买齐(GP 局内死钱——真人实证两店全购) r.continue_after_node() if logger != null: logger.log_node(node, info, r.node_index) # 冒险钱包事件采集(wallet 行族——GP 跨关延续与冒险收束 clear 的验收取数面;同日志合规转发) static func _on_adventure_wallet(event: String, gp_val: int, extra: Dictionary, st: Dictionary, logger) -> void: st.wallet_events.append({"event": String(event), "gp": int(gp_val), "amount": int(extra.get("amount", 0)), "product": String(extra.get("product", "")), "source": String(extra.get("source", ""))}) if logger != null: logger.log_wallet(event, gp_val, extra) # 三选一停点驱动(确定性策略;TP0 起 316-f 判读:1 级最稀有、同档并列取最左; # 奖励事件随 reward_events 留档) static func _on_adventure_bonus(payload: Dictionary, st: Dictionary, adr) -> void: var pl := payload.duplicate(true) pl["bonus_max_hp"] = adr.journal().bonus_total("max_hp") # 观测面:停点时刻加算区快照(回血精确断言用) st.reward_events.append({"curio_pick": pl}) var candidates: Array = payload.get("candidates", []) as Array if candidates.is_empty(): st.result = "advance_failed" return var pick: Dictionary = _pick_token(candidates, String(st.get("feat_mode", "off"))) if not adr.stop().pick_curio(String(pick.get("kind", "")), String(pick.get("tier", "")), String(pick.get("feat_id", ""))): st.result = "advance_failed" # 三选一确定性选件(TP0 316-f——1 级优先度=最稀有〔四级:legendary>rare>excellent>common〕、2 级=最左; # 候选并列同档取最左=稳定序保证。空候选/未知档位 = 回退最左防御口径。 # 成型画像(relic 模式 on:候选含 relic 令牌时优先在 relic 集内按 316-f 择一 # 〔贪心捕捞→特性入局→成型画像全流派覆盖——R3 乙单锚承载〕;off=relic 与属性加成型特性同规则竞争) static func _pick_token(candidates: Array, feat_mode: String = "off") -> Dictionary: var pool: Array = candidates if feat_mode == "on": var feats_only: Array = [] for tok in candidates: if String((tok as Dictionary).get("kind", "")) == "relic": feats_only.append(tok) if not feats_only.is_empty(): pool = feats_only var best: Dictionary = {} var best_rank := -1 for tok in pool: var t: Dictionary = tok as Dictionary var rank: int = int(TreasureItemTable.TIER_RANK.get(String(t.get("tier", "")), 0)) # 档位排序读表(未知档 0=原 match 落空同语义) if best.is_empty() or rank > best_rank: best = t best_rank = rank return best # 三选一兑付完成 → 上下文分派(关卡完成停点=推进下一关 / 奖励节点=回图选路) static func _on_adventure_curio_resolved(result: Dictionary, st: Dictionary, adr) -> void: st.reward_events.append({"curio_resolved": result.duplicate(true)}) if String(result.get("context", "")) == "reward_node": var r = adr.runner() if r == null or not r.continue_after_node(): st.result = "advance_failed" return if not adr.continue_to_next_level(): st.result = "advance_failed" # 商店贪心买齐:GP 局内死钱——买得起就买( affordable 集里挑最便宜的 # 反复买到达不上任何商品为止,guard 防死循环)。确定性(零随机流);购买出账/加算区入账全走 # runner.shop.buy_product 核心编排(与呈现层购买同一出账路径,wallet_changed 信号照常采集)。 static func _autoplay_shop(r, st: Dictionary) -> void: var guard := 30 while guard > 0: guard -= 1 var affordable: Array = [] for p in r.shop.shop_products(): if r.gp >= int(p.get("price", 1 << 30)): affordable.append(p) if affordable.is_empty(): return var pick: Dictionary = affordable[0] for p in affordable: if int(p.get("price", 1 << 30)) < int(pick.get("price", 1 << 30)): pick = p if not r.shop.buy_product(String(pick.get("product_id", ""))): return st.shop_buys = int(st.get("shop_buys", 0)) + 1 # 商店共享池贪心:货架三选一同构面贪心买齐——affordable 集挑最便宜反复买 # (每次买后全重抽、货架随买随换,枯竭/不可负担即止,guard 防死循环)。购买走 runner.shop.buy_pool_item # 核心编排(与呈现层购买同一出账+池消费路径,wallet_changed 信号照常采集)。确定性(零随机流)。 static func _autoplay_shop_pool(r, st: Dictionary) -> void: var guard := 30 while guard > 0: guard -= 1 var affordable: Array = [] for c in r.shop.shop_shelf(): if r.gp >= int(c.get("price", 1 << 30)): affordable.append(c) if affordable.is_empty(): return var pick: Dictionary = affordable[0] for c in affordable: if int(c.get("price", 1 << 30)) < int(pick.get("price", 1 << 30)): pick = c if not r.shop.buy_pool_item(String(pick.get("kind_id", ""))): return st.shop_buys = int(st.get("shop_buys", 0)) + 1 # 冒险终局(completed/failed/abandoned——result 载 summary.result,外层驱动循环随之收敛) static func _on_adventure_finished(summary: Dictionary, st: Dictionary, logger) -> void: st.summary = summary.duplicate(true) st.result = String(summary.get("result", "")) if logger != null: logger.log_level_end(summary) # 策略流 RNG:种子派生自局种子 × 黄金比率常数 + 偏移——与核心统一流 # (rng_service 单盐派生)零共享零扰动;逐局确定性保持(同种子 ⇒ 同决策序列 ⇒ 同回放) static func _policy_rng(seed_val: int) -> RandomNumberGenerator: var prng := RandomNumberGenerator.new() prng.seed = seed_val * 2654435761 + 912931 return prng # 路径轴选路(目标价值评分制):候选按节点类型评分取最高, # 同分 tie-break 沿 route 方向(min=小句柄/max=大句柄——verifier 双窗轴保持)。 # 评分语义(确定性,零随机流):rest=伤重高值(缺口比例×100)/shop=买得起高值(GP 局内死钱, # 真人实证两店全购)/reward=占位低值(MC 夹具解锁空集=零兑付直通)/boss=缓冲低值(有店有_rest # 先补给再打)/battle=基线。编号取 runner.next_candidate_ids()(编号空间解析单一出口)。 static func _route_pick(runner, route: String, st: Dictionary) -> int: var ids: Array = runner.next_candidate_ids() if ids.is_empty(): return -1 if not bool(st.get("adv", false)): # 单关机制夹具:旧机械选路保持(smoke 断言零扰动) ids.sort() return int(ids[0]) if route != "max" else int(ids[ids.size() - 1]) var nodes: Array = runner.next_candidates() var scored: Array = [] for i in nodes.size(): var node: Dictionary = nodes[i] var t := String(node.get("type", "")) var s := 30.0 if t == "rest": var max_hp := float(runner.effective_max_hp()) s = 5.0 + (1.0 - float(runner.hero_hp) / maxf(max_hp, 1.0)) * 100.0 # 满血 rest=5 低于 battle(白 rest 不去);伤重可到 105 elif t == "shop": s = 10.0 var cheapest := 1 << 30 if String(st.get("shop_mode", "legacy")) == "pool": for c in runner.shop.shop_shelf(): # pool 面:货架现价 cheapest = mini(cheapest, int(c.get("price", 1 << 30))) else: for p in runner.shop.shop_products(): cheapest = mini(cheapest, int(p.get("price", 1 << 30))) if cheapest < (1 << 30) and runner.gp >= cheapest: s = 80.0 elif t == "reward": s = 5.0 elif t == "boss": s = 20.0 scored.append({"id": int(ids[i]), "s": s}) var best: Dictionary = scored[0] for i in range(1, scored.size()): var c: Dictionary = scored[i] var better: bool if absf(c.s - best.s) > 0.0001: better = c.s > best.s else: better = (c.id < best.id) if route != "max" else (c.id > best.id) if better: best = c return int(best.id) static func _on_step_row(row: Dictionary, _runner, st: Dictionary, collect_rows: bool) -> void: if row.type == "damage" and row.source_class == "enemy_action": st.hero_dmg += int(row.value) _note_enemy(String(row.get("text", "")), st) elif row.type == "damage" and row.source_class == "mine_hit" and "全体敌人" not in String(row.get("text", "")): st.mine_dmg += int(row.value) # 踩雷直伤承伤(k 系数分母补算字段——口径与真人分析端「敌方行动+踩雷直伤」对齐) elif row.type == "cast" and row.source_class == "enemy_action": _note_enemy(String(row.get("text", "")), st) # 施放行(毒爆挂毒等——伤害行可能缺席的死亡遭遇仍可归因) if row.source_class == "mine_hit": st.mine_hits += 1 # 踩雷证据行(负向兑付——完美档断言恒 0;盲挖惩罚链埋点) # 封印经济聚合(自结算流水聚合,零新随机源): # ⚠ 行侧判据考证实录:结算段对存续增益实例每轮复发行 effect 行且复用实例 source_class #(battle_orchestrator 状态列举行)——source_class=seal_hit 的行 ≠ 标中事件。事件行唯一判据 # = 授予行专带 highlight="seal_hit"(实测 87 行 == 受理封印 87 动作,1:1 精确); # 雷区图谱追加分grant 无高光,以 display_name 后缀补捕(计入事件与合计——同封印第二份兑付)。 # 满层拒收行(reward_ineffective)零兑付不入。标空事件=归因行(type=seal_miss 每格恰一次); # 标空惩罚合计只入一次性兑付行(flat 直伤 damage/heal + gp_loss escrow 行)——sapped 授予行 # 与其状态复发行行侧不可区分不入合计(登记注记,判读侧按事件数补算)。 if row.source_class == "seal_hit" and row.type != "reward_ineffective": if String(row.get("highlight", "")) == "seal_hit" \ or String(row.get("display_name", "")).contains("雷区图谱"): st.seal_hit += 1 st.seal_hit_gain += int(row.value) if row.type == "seal_miss": st.seal_miss += 1 elif row.source_class == "seal_miss" and (row.type == "damage" or row.type == "heal" \ or String(row.get("effect_id", "")) == "gp_loss"): st.seal_miss_penalty += int(row.value) if collect_rows: st.rows.append(row) # 敌方显示名采集:敌方行动/施放行文本首词 = 单位显示名 #(攻击行「地精 撕咬(暴击)」、施放行「地精 施放 毒雾」——显示名不含空格,去重连续重复) static func _note_enemy(text: String, st: Dictionary) -> void: if text.is_empty(): return var ename := text.split(" ")[0] if ename.is_empty(): return var arr: Array = st.last_battle_enemies if arr.is_empty() or String(arr[arr.size() - 1]) != ename: arr.append(ename) static func _on_node_entered(node: Dictionary, info: Dictionary, runner, st: Dictionary, logger) -> void: var t := String(node.get("type", "")) if t == "battle" or t == "boss": st.battles += 1 st.last_battle_enemies = [] # 末场战斗敌方名序列按战斗重置(失败局终值 = 死亡遭遇) if logger != null and runner.current_battle() != null: logger.attach_battle(runner.current_battle()) if t == "boss": st.boss_depth = int(node.get("depth", -1)) st.hp_at_boss = runner.hero_hp elif t == "reward": runner.continue_after_node() # autoplay 直通不解三选一——sim 策略选择(测量期不进兑付面,非机制缺位) elif t == "rest": st.rest_healed = int(info.get("healed", 0)) st.hp_before_rest = int(info.get("hp", 0)) - st.rest_healed runner.continue_after_node() elif t == "shop": runner.continue_after_node() # 单关机制夹具(run_level)零购离开保持——smoke 商店/战斗断言面 # 不掺购买行为;冒险口径(run_adventure)已在 _on_adventure_node_entered 改贪心买齐 if logger != null: logger.log_node(node, info, runner.node_index) static func _on_level_finished(p: Dictionary, st: Dictionary, logger) -> void: st.result = String(p.get("result", "")) st.hp_left = int(p.get("hp", 0)) st.defeat_node_index = int(p.get("node_index", -1)) st.defeat_node_id = int(p.get("node_id", -1)) # 编号空间解析与行进同源(图版 = node_depth——直入口死亡节点可读面) st.defeat_node_type = String(p.get("node_type", "")) if logger != null: logger.log_level_end(p) static func _autoplay_battle(runner, settlement, st: Dictionary, logger) -> void: # 策略档经 _decide_act 单点分派(单循环承载三档——三档决策语义逐位不变,见 _decide_act)。 # 普通策略档 v3:完美策略 + 受控失误注入(标定依据=真人实证: # 零加成真人首通三关踩 23 雷)。 # v3 语义 = 会玩的普通玩家:推理与完美档同源(subset 解算/封印/卡死刷新),失误只注入两手—— # 标雷误挖(NORMAL_MINE_SLIP_P)+ 安全挖跳过(NORMAL_SKIP_DIG_P)。连续拒绝 ≥3 = 编排违约防御。 # 完美策略档:单步决策循环——move/dig/seal 经 排雷玩法 settle_batch 提交, # refresh 经 settlement.player_request_refresh(战斗结算 核心编排:行动经济 扣 RAP + 退役 + 当场换新板), # stop 出内环走统一停手提交。连续拒绝 ≥3 = 编排违约防御(fail-fast 语义,停手防死循环)。 # 上界口径:选路=目标价值评分(_route_pick)+ 商店贪心买齐(_autoplay_shop)—— # 「完美」是元游戏意义上的上界(会购物会补给),不只扫雷子游戏上界。 var round_guard := 60 while st.result == "" and runner.current_battle() == settlement and settlement.battle_result == "" and round_guard > 0: round_guard -= 1 st.rounds += 1 var start_round: int = settlement.round_index # 内层单轮循环委托 BattleDriver.drive_round(驱动循环单点——庚案收敛:本类禁再持第二轮驱动循环; # 策略分派/失误注入全保留在 _decide_act 原地,日志与计数经 _action_logger 回调承载) BattleDriver.drive_round(settlement, func() -> bool: return runner.current_battle() == settlement and settlement.battle_result == "" and settlement.action_economy.phase == "Active" and settlement.round_index == start_round, _act_provider(st), _action_logger(st, logger), 3) # 策略回调适配器(drive_round act_provider 契约——三档分派 perfect/novice/slip 全在 _decide_act 原地) static func _act_provider(st: Dictionary) -> Callable: return func(s) -> Dictionary: return _decide_act(st, s) # 日志回调适配器(drive_round on_action 契约——log_action 带轮号 + refresh/seal 计数语义逐位同原内联) static func _action_logger(st: Dictionary, logger) -> Callable: return func(kind: String, cell: Vector2i, ok: bool, ri: int) -> void: if logger != null: logger.log_action(kind, cell, ok, {"round": ri}) if ok and kind == "refresh": st.refreshes = int(st.refreshes) + 1 elif ok and kind == "seal_place": st.seal_placed = int(st.get("seal_placed", 0)) + 1 # 封印数(η 分母=全部封印数〔Q5〕;失误注入转化封印不计数=转化后实为挖) # 普通档 v3 失误注入:三类—— # ①标雷误挖:把「封印已证雷」动作直接转成「挖该格」(真人手滑原型:知道是雷、位置点错, # 踩雷吃伤且该雷奖励报废);期望踩雷增量 ≈ 每板已证雷数 × NORMAL_MINE_SLIP_P(目标完美+2/板) # ②安全挖跳过:把「挖已证安全格」动作转成本轮停手(少挖几个正确的雷——信息与推进双损失) # ③卡死赌挖:完美档无已证动作时刷新/停手拖轮次吃敌方垫刀(实证=完美档 41% 冒险成功;全败于慢玩 # 垫刀非踩雷)——真人会按概率赌一个边界格(用户实证口径「有些地方因为我看不到数字,也是盲挖 # 的」);普通档不刷新(用户自报全程没用刷新),赌挖替代刷新/停手 # 失误流 = st.prng(策略流 RNG,种子派生自局种子——同种子逐位可回放;核心七流零消费零扰动) static func _apply_normal_slips(act: Dictionary, settlement, st: Dictionary) -> Dictionary: var prng: RandomNumberGenerator = st.get("prng") if prng == null: return act var kind := String(act.get("kind", "stop")) if kind == "seal" and prng.randf() < NORMAL_MINE_SLIP_P: st.mine_slips = int(st.get("mine_slips", 0)) + 1 return {"kind": "dig", "cell": act.get("cell")} if kind == "dig": var roll := prng.randf() if roll < NORMAL_SKIP_DIG_P: st.skip_digs = int(st.get("skip_digs", 0)) + 1 return {"kind": "stop"} if roll < NORMAL_SKIP_DIG_P + NORMAL_IDLE_GAMBLE_P: var idle := AutoPlayer.gamble_pick(settlement.minesweeper) if not idle.is_empty(): return _gamble_action(idle, settlement) if kind == "refresh" or kind == "stop": var plan := AutoPlayer.gamble_pick(settlement.minesweeper) # v3.2:概率启发赌挖(choose_dig 行主序=概率最差格,实证 199/200 死第二战——退役) if not plan.is_empty(): return _gamble_action(plan, settlement) return act # 赌挖动作组装(v3.4 抽出):目标邻接=直接挖,否则先移到目标的已开邻格(下一拍再挖) static func _gamble_action(plan: Dictionary, settlement) -> Dictionary: var ms = settlement.minesweeper var hero: Vector2i = ms.hero_pos() var target: Vector2i = plan.dig_cell var d := Vector2i(absi(target.x - hero.x), absi(target.y - hero.y)) if maxi(d.x, d.y) == 1: return {"kind": "dig", "cell": target} return {"kind": "move", "cell": plan.hero_cell} # 战斗决策单行分派: # perfect 档 = 完美策略单步决策(上界口径); # normal 档 v3 = 完美+受控失误注入(冒险测量面 adv=true);单关机制夹具保持 v2 新手画像(adv 缺省)。 # 决策函数纯读板面/编排面(零 RNG 消费——失误流独走 st.prng,见 _apply_normal_slips)。 static func _decide_act(st: Dictionary, settlement) -> Dictionary: if String(st.get("policy", "normal")) == "perfect": return AutoPlayer.perfect_action(settlement.minesweeper, settlement) if bool(st.get("adv", false)): return _apply_normal_slips(AutoPlayer.perfect_action(settlement.minesweeper, settlement), settlement, st) # v3:完美+失误 return AutoPlayer.novice_action(settlement.minesweeper) # 单关机制夹具:v2 新手画像保持