# VERTICAL SLICE - NOT FOR PRODUCTION # Date: 2026-09-11(2026-09-12 增完整遥测接线——用户要求:任何模拟运行与真人试玩同日志合规: # 操作记录 / 轮次状态 / 战斗结束 / 通关结束 全量 JSONL。logger=null 时自建会话(每局一文件), # 批量调用方传共享 logger(log_run 局次标记切分)。 # 关卡自动通关策略(理性玩家近似)——呈现层数值调整板的批量模拟与 headless 扫描 # 共用同一实现,保证"面板模拟读数"与"扫描表读数"同口径。策略与 smoke_all 战斗自动玩家同族: # 挖最近可挖格 / 无可挖停手 / 奖励恢复点立即确认。 # 2026-09-23 单关直入口口径退役(用户拍板):run_level 降格为机制夹具/单战锚点专用 # (smoke_all 机制断言、rhythm/defense/damage 单战扫描)——通关率/难度类测量禁用本入口 # (满血零上下文进关,与真实冒险条件通关率脱节),一律走 run_adventure。 # 2026-09-15 格面资源批:道具窗口选择与可选类获取计数随可选类道具删除下线。 # 2026-09-14 增完美策略档(policy="perfect",默认 "normal" 零行为变更——既有调用方签名兼容): # 零盲挖/全标旗/卡死刷新——上界测量仪,求解器与动作语义见 auto_player.gd 完美策略档节。 # 2026-09-17 分支节点图批:选路适配(零自动推进)——兑付确认/战斗胜利后 runner 进选路态, # 驱动循环按 route 策略显式 pick_next_node;route = "min"/"max"(候选节点编号最小/最大—— # 确定性路径轴,零随机流自开;默认 "min" 既有调用方签名兼容)。 # 2026-09-21 冒险结构实现批:run_adventure 三关连探驱动(AdventureRunner——主种子恰一次、 # 通关兑付停点确认、adventure_finished 置 result/summary;外层每关重取 runner 实例经 _drive_level # 复用单关驱动)。run_level 驱动段抽成 _drive_level——签名与返回结构零变化,行为逐位不变。 # 2026-10-01 迭代 10 奖励重构批:大奖励立即确认改三选一停点驱动(候选取首件/替换放弃新件)。 # 2026-10-01 模拟档位重构(master 数值修复批——普通档 v2 被真人实证推翻,尺子比真人弱 20 倍): # ①普通档 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://smoke/auto_player.gd") # O4 收口:自动玩家策略单一实现 # ── 普通档 v3 失误注入参数(2026-10-01 用户规格:踩雷期望=完美+2/板、每板少挖几个正确格)── # 标雷→误挖转化率:v3.2 实证 0.22 时普通档 0/100 全灭(首轮大量已证旗=保底命中,非真人失误 # 形态——真人几乎不挖自己插旗的格);降 0.10 = 偶发真手滑,主踩雷源=卡死赌挖(不确定性概率命中) const NORMAL_MINE_SLIP_P := 0.10 # 安全挖跳过率:每板安全格 ~70-90 × p ≈ 3-5 次漏挖(信息与推进双损失) const NORMAL_SKIP_DIG_P := 0.05 # 空闲提前赌挖率(v3.4):已证安全格在手时小概率先赌一格——完美系把不确定性攒到残局 # (残局 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", template_pick: int = -1) -> Dictionary: var own_session := logger == null if own_session: logger = load("res://presentation/playtest_logger.gd").new() logger.start_session(seed_val) logger.ap_per_round = int(db.action_economy.ap_per_round) 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": [], # 旗经济五字段(迭代14②判式换代批 T1——runsum 读数面;聚合自结算流水 source_class, # 零新随机源零核心改动;η=flag_hit÷flag_placed 与标空率派生归分析侧) "flag_placed": 0, "flag_hit": 0, "flag_miss": 0, "flag_hit_gain": 0, "flag_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, {}, template_pick): 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) # 停点前移批 2026-09-23:入口停点(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) # 三关连探冒险驱动(冒险结构实现批 2026-09-21)——AdventureRunner 编排:主种子恰一次 # (start_adventure 唯一 new_adventure 调用点)、大奖励立即确认、冒险终局置 result/summary。 # 返回结构 = run_level 同族 + summary / wallet_events / encounter_max_hp_seq / reward_events / # visited_ids(全冒险节点行进序列——跨关累计,非末关 runner 的 visited 随实例更替,此处为唯一全量面)。 # 2026-09-22 数值验证工具批:增 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.start_session(seed_val) logger.ap_per_round = int(db.action_economy.ap_per_round) 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": [], "relic_mode": String(relic.get("mode", "off")), # 特性分层开关(迭代 12 T7——off=316-f 通用竞争/on=成型画像优先捕捞 relic) "shop_gp_seq": [], # 商店房到达余额序(逐关 1 次——GP 跨关延续验收取数面) # 旗经济五字段(迭代14②判式换代批 T1——与 run_level 同口径,见彼处注释) "flag_placed": 0, "flag_hit": 0, "flag_miss": 0, "flag_hit_gain": 0, "flag_miss_penalty": 0, "prng": _policy_rng(seed_val), "mine_slips": 0, "skip_digs": 0, "shop_buys": 0, "adv": true, # v3 档位行为开关(选路评分/失误注入/商店购买只挂冒险测量面——单关夹具保持旧语义零扰动) "level_templates": [], # 逐关图模板名(关卡节点图生成 v0.4 分层出数) } adr.step_row.connect(_on_step_row.bind(adr, st, collect_rows)) adr.node_entered.connect(_on_adventure_node_entered.bind(adr, st, logger)) adr.wallet_changed.connect(_on_adventure_wallet.bind(st, logger)) # 三选一停点驱动(迭代 10 奖励重构批——big_reward_paid 退役:确定性策略候选取首件、 # 道具件满位替换放弃新件、兑付后推进下一关——零随机流自开,同种子逐位可复放) adr.bonus_choice_presented.connect(_on_adventure_bonus.bind(st, adr)) adr.bonus_resolved.connect(_on_adventure_bonus_resolved.bind(st, adr)) adr.aux_replace_presented.connect(_on_adventure_aux_replace.bind(st, adr)) adr.adventure_finished.connect(_on_adventure_finished.bind(st, logger)) adr.set_hero_override(atk_pct, hp_pct) # 同调分层注入(迭代 11 第①批 T8——缺省 {} 与既有调用方逐位一致零行为变更) var adv_params := {"hero_override": hero_override, "first_script": false} if not attune.is_empty(): adv_params["attunement"] = attune.get("equipped", []) adv_params["attune_refresh_y"] = int(attune.get("refresh_y", 0)) # 特性注入(迭代 12 T7——formed 画像=开局注入七流派各 2 件档一成型集;测量面专用) if relic.has("seed_ids"): adv_params["relics_seed"] = relic.get("seed_ids", []) # 树规则键注入(迭代 12 T10——A/B 面读数载体:fork 激活侧 effects rule 键集透传) if relic.has("tree_rules"): adv_params["tree_rules"] = relic.get("tree_rules", []) # 商店分层注入(迭代 11 第②批 T5——缺省 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: # 终局池读数(迭代 11 第②批 T5——摘要 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() # 特性终局持有集(迭代 12 T1——摘要 additive 键清账前快照,同 pool_stock 模式) st.log_path = logger.log_path() if own_session: logger.close_session() return st # 冒险模式节点采集(_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_templates"): st["level_templates"] = [] st["level_templates"].append(r.active_template_name) # MC 分层出数(关卡×图模板——v0.4 测量对账条款) 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("node_depth", -1)) st.hp_at_boss = int(adr.ledger["hero_hp"]) elif t == "reward": # 奖励节点三选一(迭代 10 追加批——共享池确定性策略;TP0 起 316-f 判读:1 级最稀有、 # 同档并列取最左;替换放弃新件;兑付后 continue_after_node 回图选路) var ndraw: Dictionary = adr.node_bonus_candidates() var ncands: Array = ndraw.get("candidates", []) as Array st.reward_events.append({"node_bonus_pick": ncands.duplicate(true)}) if ncands.is_empty(): r.continue_after_node() # 牌库空合法态直通(池耗即枯竭——防御保留) else: adr.present_node_bonus(ncands, ndraw.get("aux_detail", []) as Array) var npick: Dictionary = _pick_token(ncands, String(st.get("relic_mode", "off"))) if adr.apply_node_bonus(String(npick.get("kind", "")), String(npick.get("tier", "")), String(npick.get("relic_id", ""))): if not adr._node_bonus_pending.is_empty() and adr._node_bonus_pending.has("incoming"): adr.resolve_bonus_replace(-1) # 满位替换:放弃新件 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) # 共享池货架贪心(迭代 11 第②批 T5——买一件=奖励面少一件的机会成本画像维持贪心语义) else: _autoplay_shop(r, st) # 贪心买齐(2026-10-01——旧零购离开退役: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) # 三选一停点驱动(迭代 10——确定性策略;TP0 起 316-f 判读:1 级最稀有、同档并列取最左; # 奖励事件随 reward_events 留档) static func _on_adventure_bonus(payload: Dictionary, st: Dictionary, adr) -> void: st.reward_events.append({"bonus_pick": payload.duplicate(true)}) 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("relic_mode", "off"))) if not adr.pick_bonus(String(pick.get("kind", "")), String(pick.get("tier", "")), String(pick.get("relic_id", ""))): st.result = "advance_failed" # 三选一确定性选件(TP0 316-f——1 级优先度=最稀有〔档位最高 rarest>rare>common〕、2 级=最左; # 候选并列同档取最左=稳定序保证。空候选/未知档位 = 回退最左防御口径。 # 成型画像(迭代 12 T7——relic 模式 on:候选含 relic 令牌时优先在 relic 集内按 316-f 择一 # 〔贪心捕捞→特性入局→成型画像全流派覆盖——R3 乙单锚承载〕;off=relic 与属性加成型特性同规则竞争) static func _pick_token(candidates: Array, relic_mode: String = "off") -> Dictionary: var pool: Array = candidates if relic_mode == "on": var relics_only: Array = [] for tok in candidates: if String((tok as Dictionary).get("kind", "")) == "relic": relics_only.append(tok) if not relics_only.is_empty(): pool = relics_only var best: Dictionary = {} var best_rank := -1 for tok in pool: var t: Dictionary = tok as Dictionary var rank := 0 match String(t.get("tier", "")): "rarest": rank = 3 "rare": rank = 2 "common": rank = 1 if best.is_empty() or rank > best_rank: best = t best_rank = rank return best # 同调位满位替换子交互(确定性策略:放弃新件不获取——分发面按停点上下文裁决节点/通关渠道) static func _on_adventure_aux_replace(items: Array, incoming: Dictionary, st: Dictionary, adr) -> void: st.reward_events.append({"aux_replace": incoming.duplicate(true)}) if not adr.resolve_bonus_replace(-1): st.result = "advance_failed" # 三选一兑付完成 → 上下文分派(通关停点=推进下一关 / 奖励节点=回图选路) static func _on_adventure_bonus_resolved(result: Dictionary, st: Dictionary, adr) -> void: st.reward_events.append({"bonus_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" # 商店贪心买齐(2026-10-01 模拟档位重构):GP 局内死钱——买得起就买( affordable 集里挑最便宜的 # 反复买到达不上任何商品为止,guard 防死循环)。确定性(零随机流);购买出账/加算区入账全走 # runner.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_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.buy_product(String(pick.get("product_id", ""))): return st.shop_buys = int(st.get("shop_buys", 0)) + 1 # 商店共享池贪心(迭代 11 第②批 T5):货架三选一同构面贪心买齐——affordable 集挑最便宜反复买 # (每次买后全重抽、货架随买随换,枯竭/不可负担即止,guard 防死循环)。购买走 runner.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_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.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(2026-10-01 模拟档位重构):种子派生自局种子 × 黄金比率常数 + 偏移——与核心统一流 # (rng_service 单盐派生)零共享零扰动;逐局确定性保持(同种子 ⇒ 同决策序列 ⇒ 同回放) static func _policy_rng(seed_val: int) -> RandomNumberGenerator: var prng := RandomNumberGenerator.new() prng.seed = seed_val * 2654435761 + 912931 return prng # 路径轴选路(2026-10-01 目标价值评分制——机械最小编号退役):候选按节点类型评分取最高, # 同分 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_shelf(): # pool 面:货架现价(迭代 11 第②批 T5) cheapest = mini(cheapest, int(c.get("price", 1 << 30))) else: for p in runner.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;盲挖惩罚链埋点) # 旗经济聚合(迭代14②判式换代批 T1——自结算流水聚合,零新随机源): # ⚠ 行侧判据考证实录:结算段对存续增益实例每轮复发行 effect 行且复用实例 source_class #(battle_orchestrator 状态列举行)——source_class=flag_hit 的行 ≠ 标中事件。事件行唯一判据 # = 授予行专带 highlight="flag_hit"(实测 87 行 == 受理插旗 87 动作,1:1 精确); # 雷区图谱追加分grant 无高光,以 display_name 后缀补捕(计入事件与合计——同旗第二份兑付)。 # 满层拒收行(reward_ineffective)零兑付不入。标空事件=归因行(type=flag_miss 每格恰一次); # 标空惩罚合计只入一次性兑付行(flat 直伤 damage/heal + gp_loss escrow 行)——sapped 授予行 # 与其状态复发行行侧不可区分不入合计(登记注记,判读侧按事件数补算)。 if row.source_class == "flag_hit" and row.type != "reward_ineffective": if String(row.get("highlight", "")) == "flag_hit" \ or String(row.get("display_name", "")).contains("雷区图谱"): st.flag_hit += 1 st.flag_hit_gain += int(row.value) if row.type == "flag_miss": st.flag_miss += 1 elif row.source_class == "flag_miss" and (row.type == "damage" or row.type == "heal" \ or String(row.get("effect_id", "")) == "gp_loss"): st.flag_miss_penalty += int(row.value) if collect_rows: st.rows.append(row) # 敌方显示名采集(数值验证工具批 2026-09-22):敌方行动/施放行文本首词 = 单位显示名 #(攻击行「地精 撕咬(暴击)」、施放行「地精 施放 毒雾」——显示名不含空格,去重连续重复) 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("node_depth", -1)) st.hp_at_boss = runner.hero_hp elif t == "reward": runner.continue_after_node() # 奖励点 = 内容待拍板直通节点(格面资源批 2026-09-15) 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 单点分派(T6 样板收口批 2026-10-02——原 _autoplay_battle/_autoplay_battle_perfect # 双循环逐行相同仅差 act 来源行,合并单循环;三档决策语义逐位不变,见 _decide_act)。 # 普通策略档 v3(2026-10-01 用户重定义):完美策略 + 受控失误注入——v2「新手级单步推理+盲挖 # 兜底」画像被真人实证推翻(零加成真人首通三关踩 23 雷 vs v2 普通档 L2 条件通关率 10.6%)。 # v3 语义 = 会玩的普通玩家:推理与完美档同源(subset 解算/标旗/卡死刷新),失误只注入两手—— # 标雷误挖(NORMAL_MINE_SLIP_P)+ 安全挖跳过(NORMAL_SKIP_DIG_P)。连续拒绝 ≥3 = 编排违约防御。 # 完美策略档(2026-09-14):单步决策循环——move/dig/flag 经 排雷玩法 settle_batch 提交, # refresh 经 settlement.player_request_refresh(战斗结算 核心编排:行动经济 扣 RAP + 退役 + 当场换新板), # stop 出内环走统一停手提交。连续拒绝 ≥3 = 编排违约防御(fail-fast 语义,停手防死循环)。 # 2026-10-01 上界修正:选路改目标价值评分(_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 var action_guard := 400 var rejects := 0 while runner.current_battle() == settlement and settlement.battle_result == "" and settlement.action_economy.phase == "Active" \ and settlement.round_index == start_round and action_guard > 0 and rejects < 3: action_guard -= 1 var act: Dictionary = _decide_act(st, settlement) var kind := String(act.get("kind", "stop")) if kind == "stop": break var ok := false if kind == "move": ok = _submit_intent(settlement, "move", act.get("cell"), logger, start_round) elif kind == "dig": ok = _submit_intent(settlement, "dig", act.get("cell"), logger, start_round) elif kind == "flag": ok = _submit_intent(settlement, "flag_place", act.get("cell"), logger, start_round) elif kind == "refresh": ok = bool(settlement.player_request_refresh()) if logger != null: logger.log_action("refresh", Vector2i(-1, -1), ok, {"round": start_round}) if ok: if kind == "refresh": st.refreshes = int(st.refreshes) + 1 elif kind == "flag": st.flag_placed = int(st.get("flag_placed", 0)) + 1 # 插旗数(η 分母=全部插旗数——迭代14② Q5 裁决;失误注入转化旗不计数=转化后实为挖) rejects = 0 else: rejects += 1 if runner.current_battle() == settlement and settlement.battle_result == "" and settlement.action_economy.phase == "Active" and settlement.round_index == start_round: var out_s: Array = settlement.minesweeper.settle_batch([{"action": "stop"}]) if logger != null: logger.log_action("stop", Vector2i(-1, -1), _accepted(out_s), {"round": start_round}) # 普通档 v3 失误注入(2026-10-01 用户规格):三类—— # ①标雷误挖:把「标旗已证雷」动作直接转成「挖该格」(真人手滑原型:知道是雷、位置点错, # 踩雷吃伤且该雷奖励报废);期望踩雷增量 ≈ 每板已证雷数 × 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 == "flag" 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} # 战斗决策单行分派(T6 双循环合并抽取——原两循环唯一差异行): # 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 新手画像保持 static func _submit_intent(settlement, action: String, cell, logger, round_index: int) -> bool: var out: Array = settlement.minesweeper.settle_batch([{"action": action, "cell": cell}]) if logger != null: logger.log_action(action, cell, _accepted(out), {"round": round_index}) return _accepted(out) static func _accepted(outcomes: Array) -> bool: return not outcomes.is_empty() and bool(outcomes[0].accepted)