# 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——主种子恰一次、 # 大奖励立即确认 continue_to_next_level、adventure_finished 置 result/summary;外层每关重取 # runner 实例经 _drive_level 复用单关驱动)。run_level 驱动段抽成 _drive_level——签名与 # 返回结构零变化,行为逐位不变。 # 运行前提:db 已 load_tables;run_level/run_adventure 均须全新 RngService(同种子 ⇒ 全程逐步可回放)。 # 实现注:信号处理体全部为静态方法 + bind 传状态——GDScript 多行 lambda 内嵌 match/控制流的解析不可靠。 extends RefCounted const AutoPlayer := preload("res://smoke/auto_player.gd") # O4 收口:自动玩家策略单一实现 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, "visited_ids": [], "choices": 0, "last_battle_enemies": [], } 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)) 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 = {}) -> 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, "visited_ids": [], "choices": 0, "last_battle_enemies": [], "summary": {}, "wallet_events": [], "encounter_max_hp_seq": [], "reward_events": [], "shop_gp_seq": [], # 商店房到达余额序(逐关 1 次——GP 跨关延续验收取数面) "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)) adr.big_reward_paid.connect(_on_adventure_reward.bind(st, adr)) adr.adventure_finished.connect(_on_adventure_finished.bind(st, logger)) adr.set_hero_override(atk_pct, hp_pct) if not adr.start_adventure(seed_val, {"hero_override": hero_override, "first_script": false}): # 测量夹具恒抽签(v0.4——首次脚本分支不进任何读数) st.result = "start_failed" if own_session: logger.close_session() return st var guard_drives := 10 # 每次外层迭代 = 驱动一关(关卡更替由 big_reward 确认与 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() 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": r.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 r.continue_after_node() elif t == "shop": st.shop_gp_seq.append(int(info.get("gp", -1))) r.continue_after_node() # 商店房零购离开(自动路径直通 Boss——购买行为归冒烟专项/真人试玩) 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", ""))}) if logger != null: logger.log_wallet(event, gp_val, extra) # 大奖励立即确认(非末关通关 → continue_to_next_level 进下一关——自动路径无大奖励屏停点) static func _on_adventure_reward(reward: Dictionary, st: Dictionary, adr) -> void: st.reward_events.append(reward.duplicate(true)) if not adr.continue_to_next_level(): st.result = "advance_failed" # 冒险终局(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) # 路径轴策略(确定性——候选编号最小/最大;min 侧 = 岔路恒取小编号支)。 # 编号取 runner.next_candidate_ids()(编号空间解析单一出口——单链下标 / 图 node_depth) static func _route_pick(runner, route: String) -> int: var ids: Array = runner.next_candidate_ids() if ids.is_empty(): return -1 ids.sort() return ids[0] if route != "max" else ids[ids.size() - 1] 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 == "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;盲挖惩罚链埋点) 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() # 商店房零购离开(GP+商店房批——自动路径直通 Boss;购买行为归冒烟商店专项断言/真人试玩) 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: if String(st.get("policy", "normal")) == "perfect": _autoplay_battle_perfect(runner, settlement, st, logger) return # 普通策略档 v2(2026-09-19 用户拍板):基础定式画像——单步决策循环(move/dig/flag 经 settle_batch 提交, # 推理分级见 auto_player.gd 普通策略档节:新手级单数字满足规则 + 盲挖边界兜底、无 refresh); # 旧"无推理挖最近格"口径随批退役。连续拒绝 ≥3 = 编排违约防御(同完美档 fail-fast 语义)。 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 := AutoPlayer.novice_action(settlement.minesweeper) 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) if ok: 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}) # 完美策略档战斗驱动(2026-09-14):单步决策循环——move/dig/flag 经 排雷玩法 settle_batch 提交, # refresh 经 settlement.player_request_refresh(战斗结算 核心编排:行动经济 扣 RAP + 退役 + 当场换新板), # stop 出内环走统一停手提交。连续拒绝 ≥3 = 编排违约防御(fail-fast 语义,停手防死循环)。 static func _autoplay_battle_perfect(runner, settlement, st: Dictionary, logger) -> void: 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 := AutoPlayer.perfect_action(settlement.minesweeper, 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 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}) 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)