Merge branch 'master' into m3-iter10-20261001
# Conflicts: # game/smoke/level_autoplay.gd # game/smoke/smoke_all.gd
This commit is contained in:
commit
bc737fb0cb
24 files changed
+1471
-112
No files matched your search
@@ -208,3 +208,45 @@ static func _row_major(cells: Dictionary) -> Array:
|
||||
var out: Array = cells.keys()
|
||||
out.sort_custom(func(a, b): return a.y * 1000 + a.x < b.y * 1000 + b.x)
|
||||
return out
|
||||
|
||||
|
||||
# 卡死赌挖选格(普通档 v3.1 2026-10-01——模拟档位重构):概率启发近似真人「挑看起来最安全的一格」。
|
||||
# 危险度 = 该格对每个邻接已证数字约束取(余雷数/未知数)比值中的最小值(= 任一邻数字视角下的
|
||||
# 最低雷占比——single 已证安全格在进入本函数前已被 perfect_action 消化,故卡死态比值恒 >0);
|
||||
# 同危险度行主序先到先得。纯查询面零 RNG(确定性保回放);寻路同 choose_dig(目标已开 8 邻
|
||||
# 首个可达者为 hero_cell)。
|
||||
static func gamble_pick(minesweeper) -> Dictionary:
|
||||
var cons := _constraints(minesweeper, minesweeper.current_board(), {})
|
||||
var best_h := 2.0
|
||||
var best := Vector2i(-1, -1)
|
||||
var revealed: Array = minesweeper.revealed_cells()
|
||||
var revealed_set := {}
|
||||
for c in revealed:
|
||||
revealed_set[c] = true
|
||||
var board = minesweeper.current_board()
|
||||
for y in board.height:
|
||||
for x in board.width:
|
||||
var c := Vector2i(x, y)
|
||||
if minesweeper.is_revealed(c) or minesweeper.has_flag(c):
|
||||
continue
|
||||
var adj := false
|
||||
for n in board.neighbors8(c):
|
||||
if revealed_set.has(n):
|
||||
adj = true
|
||||
break
|
||||
if not adj:
|
||||
continue
|
||||
var h := 1.0
|
||||
for con in cons:
|
||||
if (con["unknown"] as Dictionary).has(c):
|
||||
var ratio := float(con["mines"]) / float((con["unknown"] as Dictionary).size())
|
||||
h = minf(h, ratio)
|
||||
if h < best_h - 0.0001:
|
||||
best_h = h
|
||||
best = c
|
||||
if best.x < 0:
|
||||
return {}
|
||||
for n in board.neighbors8(best):
|
||||
if minesweeper.is_revealed(n) and (n == minesweeper.hero_pos() or not minesweeper.compute_move_path(n).is_empty()):
|
||||
return {"dig_cell": best, "hero_cell": n}
|
||||
return {}
|
||||
@@ -0,0 +1,62 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# 棋盘渲染帧取证探针(2026-10-01——数字墨族按板自适应修复的目视验证面:机检≠看得清)。
|
||||
# 必须**窗口化**运行(headless 无渲染面):
|
||||
# Godot_console.exe --path . res://smoke/board_shot_probe.tscn -- <输出目录>
|
||||
# 流程:L1/L2/L3 各起一板(真实 LevelRunner+settle_batch 驱动 ~18 步 novice——自然揭示数字/旗),
|
||||
# BoardView 真渲染,frame_post_draw 同步后抓视口 PNG。零存档触碰(无 save_manager 引用)。
|
||||
extends Node
|
||||
|
||||
const AutoPlayer := preload("res://smoke/auto_player.gd")
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var args := OS.get_cmdline_user_args()
|
||||
var out := args[0] if args.size() >= 1 and args[0] != "" else "user://board-shots"
|
||||
DirAccess.make_dir_recursive_absolute(out)
|
||||
await get_tree().process_frame
|
||||
var db = load("res://core/config_db.gd").new()
|
||||
var ok: bool = db.load_tables("res://data/tables", true)
|
||||
if not ok:
|
||||
push_error("[board_shot] 表加载失败")
|
||||
get_tree().quit(1)
|
||||
return
|
||||
var ui_font: Font = load("res://presentation/views/ui_kit.gd").load_font(load("res://presentation/views/ui_kit.gd").FONT_UI)
|
||||
var targets := [[1, "L1_dungeon"], [2, "L2_underdark"], [3, "L3_deep_underdark"]]
|
||||
for t in targets:
|
||||
var level_index: int = t[0]
|
||||
var tag: String = t[1]
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(420000 + level_index)
|
||||
var runner = load("res://core/level_runner.gd").new(db, rng)
|
||||
if not runner.start_level(level_index, 420000 + level_index, {}, 0):
|
||||
push_error("[board_shot] 开关失败 level=%d" % level_index)
|
||||
get_tree().quit(1)
|
||||
return
|
||||
var settlement = runner.current_battle()
|
||||
for i in range(18):
|
||||
var act: Dictionary = AutoPlayer.novice_action(settlement.minesweeper)
|
||||
var kind := String(act.get("kind", "stop"))
|
||||
if kind == "stop":
|
||||
break
|
||||
var op := kind
|
||||
if kind == "flag":
|
||||
op = "flag_place"
|
||||
settlement.minesweeper.settle_batch([{"action": op, "cell": act.get("cell")}])
|
||||
var view = load("res://presentation/views/board_view.gd").new()
|
||||
view.size = Vector2(900, 900)
|
||||
view.position = Vector2(40, 40)
|
||||
if ui_font != null:
|
||||
view.set_world_font(ui_font)
|
||||
add_child(view)
|
||||
view.rebuild(settlement.minesweeper.current_board(), settlement.minesweeper)
|
||||
for i in range(6):
|
||||
await get_tree().process_frame
|
||||
await RenderingServer.frame_post_draw
|
||||
var img := get_viewport().get_texture().get_image()
|
||||
var path := "%s/board_%s.png" % [out, tag]
|
||||
img.save_png(path)
|
||||
print("[board_shot] saved %s (%dx%d)" % [path, img.get_width(), img.get_height()])
|
||||
view.queue_free()
|
||||
await get_tree().process_frame
|
||||
print("[board_shot] done")
|
||||
get_tree().quit(0)
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://smoke/board_shot_probe.gd" id="1"]
|
||||
|
||||
[node name="BoardShotProbe" type="Node"]
|
||||
script = ExtResource("1")
|
||||
+155
-12
@@ -15,15 +15,31 @@
|
||||
# 驱动循环按 route 策略显式 pick_next_node;route = "min"/"max"(候选节点编号最小/最大——
|
||||
# 确定性路径轴,零随机流自开;默认 "min" 既有调用方签名兼容)。
|
||||
# 2026-09-21 冒险结构实现批:run_adventure 三关连探驱动(AdventureRunner——主种子恰一次、
|
||||
# 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
|
||||
@@ -42,6 +58,7 @@ static func run_level(db, seed_val: int, level_index: int, atk_pct: int, hp_pct:
|
||||
"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": [],
|
||||
"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))
|
||||
@@ -80,7 +97,7 @@ static func _drive_level(runner, st: Dictionary, logger, policy: String, route:
|
||||
# 选路态(入口停点 / 兑付确认 / 战斗胜利后的零自动推进停点)——route 策略显式选下一跳
|
||||
if runner.node_index >= 0:
|
||||
st.choices = int(st.choices) + 1
|
||||
runner.pick_next_node(_route_pick(runner, route))
|
||||
runner.pick_next_node(_route_pick(runner, route, st))
|
||||
continue
|
||||
guard_battles -= 1
|
||||
var this_settlement = runner.current_battle()
|
||||
@@ -118,6 +135,8 @@ static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_
|
||||
"visited_ids": [], "choices": 0, "last_battle_enemies": [],
|
||||
"summary": {}, "wallet_events": [], "encounter_max_hp_seq": [], "reward_events": [],
|
||||
"shop_gp_seq": [], # 商店房到达余额序(逐关 1 次——GP 跨关延续验收取数面)
|
||||
"prng": _policy_rng(seed_val), "mine_slips": 0, "skip_digs": 0, "shop_buys": 0,
|
||||
"adv": true, # v3 档位行为开关(选路评分/失误注入/商店购买只挂冒险测量面——单关夹具保持旧语义零扰动)
|
||||
|
||||
"level_templates": [], # 逐关图模板名(关卡节点图生成 v0.4 分层出数)
|
||||
}
|
||||
@@ -192,7 +211,8 @@ static func _on_adventure_node_entered(node: Dictionary, info: Dictionary, adr,
|
||||
r.continue_after_node()
|
||||
elif t == "shop":
|
||||
st.shop_gp_seq.append(int(info.get("gp", -1)))
|
||||
r.continue_after_node() # 商店房零购离开(自动路径直通 Boss——购买行为归冒烟专项/真人试玩)
|
||||
_autoplay_shop(r, st) # 贪心买齐(2026-10-01——旧零购离开退役:GP 局内死钱,真人实证两店全购)
|
||||
r.continue_after_node()
|
||||
if logger != null:
|
||||
logger.log_node(node, info, r.node_index)
|
||||
|
||||
@@ -233,6 +253,28 @@ static func _on_adventure_bonus_resolved(result: Dictionary, st: Dictionary, adr
|
||||
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
|
||||
|
||||
|
||||
# 冒险终局(completed/failed/abandoned——result 载 summary.result,外层驱动循环随之收敛)
|
||||
static func _on_adventure_finished(summary: Dictionary, st: Dictionary, logger) -> void:
|
||||
st.summary = summary.duplicate(true)
|
||||
@@ -241,14 +283,58 @@ static func _on_adventure_finished(summary: Dictionary, st: Dictionary, logger)
|
||||
logger.log_level_end(summary)
|
||||
|
||||
|
||||
# 路径轴策略(确定性——候选编号最小/最大;min 侧 = 岔路恒取小编号支)。
|
||||
# 编号取 runner.next_candidate_ids()(编号空间解析单一出口——单链下标 / 图 node_depth)
|
||||
static func _route_pick(runner, route: String) -> int:
|
||||
# 策略流 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
|
||||
ids.sort()
|
||||
return ids[0] if route != "max" else ids[ids.size() - 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
|
||||
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:
|
||||
@@ -293,7 +379,8 @@ static func _on_node_entered(node: Dictionary, info: Dictionary, runner, st: Dic
|
||||
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;购买行为归冒烟商店专项断言/真人试玩)
|
||||
runner.continue_after_node() # 单关机制夹具(run_level)零购离开保持——smoke 商店/战斗断言面
|
||||
# 不掺购买行为;冒险口径(run_adventure)已在 _on_adventure_node_entered 改贪心买齐
|
||||
if logger != null:
|
||||
logger.log_node(node, info, runner.node_index)
|
||||
|
||||
@@ -312,9 +399,10 @@ 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 语义)。
|
||||
# 普通策略档 v3(2026-10-01 用户重定义):完美策略 + 受控失误注入——v2「新手级单步推理+盲挖
|
||||
# 兜底」画像被真人实证推翻(零加成真人首通三关踩 23 雷 vs v2 普通档 L2 条件通关率 10.6%)。
|
||||
# v3 语义 = 会玩的普通玩家:推理与完美档同源(subset 解算/标旗/卡死刷新),失误只注入两手——
|
||||
# 标雷误挖(NORMAL_MINE_SLIP_P)+ 安全挖跳过(NORMAL_SKIP_DIG_P)。连续拒绝 ≥3 = 编排违约防御。
|
||||
var round_guard := 60
|
||||
while st.result == "" and runner.current_battle() == settlement and settlement.battle_result == "" and round_guard > 0:
|
||||
round_guard -= 1
|
||||
@@ -325,7 +413,11 @@ static func _autoplay_battle(runner, settlement, st: Dictionary, logger) -> void
|
||||
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 act: Dictionary
|
||||
if bool(st.get("adv", false)):
|
||||
act = _apply_normal_slips(AutoPlayer.perfect_action(settlement.minesweeper, settlement), settlement, st) # v3:完美+失误
|
||||
else:
|
||||
act = AutoPlayer.novice_action(settlement.minesweeper) # 单关机制夹具:v2 新手画像保持
|
||||
var kind := String(act.get("kind", "stop"))
|
||||
if kind == "stop":
|
||||
break
|
||||
@@ -336,7 +428,13 @@ static func _autoplay_battle(runner, settlement, st: Dictionary, logger) -> void
|
||||
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
|
||||
@@ -346,9 +444,54 @@ static func _autoplay_battle(runner, settlement, st: Dictionary, logger) -> void
|
||||
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}
|
||||
|
||||
|
||||
# 完美策略档战斗驱动(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)——
|
||||
# 「完美」须是元游戏意义上的上界(会购物会补给),不再只是扫雷子游戏上界。
|
||||
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:
|
||||
|
||||
+18
-16
@@ -331,8 +331,8 @@ func _smoke_adventure() -> void:
|
||||
var resolve_ok := level_resolves.size() == 2
|
||||
var max_flat := int(_db.adventure.bonus_max_hp_flat)
|
||||
var heal_pct := int(_db.adventure.clear_heal_pct)
|
||||
var hp_acc := 0 # max_hp 件累加(回血封顶断言的生效上限推导)
|
||||
var ks_prefix := 0
|
||||
hp_acc := 0 # max_hp 件累加(回血封顶断言的生效上限推导)
|
||||
ks_prefix := 0
|
||||
for i in picks.size():
|
||||
var p: Dictionary = picks[i]
|
||||
var cands: Array = p.get("candidates", []) as Array
|
||||
@@ -346,37 +346,39 @@ func _smoke_adventure() -> void:
|
||||
var heal: int = int(p.get("heal_gained", -1))
|
||||
var hp_before: int = int(p.get("hp_before", -1))
|
||||
var hp_after: int = int(p.get("hp_after", -1))
|
||||
var max_eff := int(_db.hero.max_hp) + hp_acc # 回血口径 = 表值+加算区(无 TunePanel 百分比)
|
||||
if heal != mini(int(floor(float(max_eff) * float(heal_pct) / 100.0)), max_eff - hp_before) \
|
||||
or hp_after != hp_before + heal or hp_after > max_eff:
|
||||
# 模拟档 v3.4 商店贪心购血合法抬升(L2/L3 店在 Boss 前)——回血口径窗口 = 加算区件 + 购血余量 0..15
|
||||
var heal_ok_i := false
|
||||
for extra in 16:
|
||||
var m := int(_db.hero.max_hp) + hp_acc + extra
|
||||
if heal == mini(int(floor(float(m) * float(heal_pct) / 100.0)), m - hp_before):
|
||||
heal_ok_i = true
|
||||
break
|
||||
if not heal_ok_i or hp_after != hp_before + heal or hp_after > int(_db.hero.max_hp) + hp_acc + 15:
|
||||
heal_ok = false
|
||||
ks_prefix += int(_db.meta_boss_keystone_row(int(p.get("level_index", -1))).get("first_kill", 0))
|
||||
if int(p.get("keystone_gained", -1)) != int(_db.meta_boss_keystone_row(int(p.get("level_index", -1))).get("first_kill", 0)) \
|
||||
or int(p.get("keystone_total", -1)) != ks_prefix:
|
||||
if int(p.get("keystone_gained", -1)) != int(_db.meta_boss_keystone_row(int(p.get("level_index", -1))).get("first_kill", 0)) or int(p.get("keystone_total", -1)) != ks_prefix:
|
||||
ks_gain_ok = false
|
||||
if resolve_ok:
|
||||
var r: Dictionary = level_resolves[i]
|
||||
if not (p.get("candidates", []) as Array).has(String(r.get("picked", ""))) \
|
||||
or int(r.get("value", -1)) != int(_db.adventure.bonus_value(String(r.get("picked", "")))):
|
||||
if not (p.get("candidates", []) as Array).has(String(r.get("picked", ""))) or int(r.get("value", -1)) != int(_db.adventure.bonus_value(String(r.get("picked", "")))):
|
||||
resolve_ok = false
|
||||
if String(r.get("picked", "")) == "max_hp":
|
||||
hp_acc += max_flat
|
||||
check(cand_ok, "三选一候选 2–3 件且零重复(加权洗牌不放回)")
|
||||
check(heal_ok, "通关部分回血封顶(floor(上限×比例) 封顶缺口——血回满退役)")
|
||||
check(heal_ok, "通关部分回血封顶(floor(上限×比例) 封顶缺口——血回满退役;购血容差窗口)")
|
||||
check(ks_gain_ok, "钥石逐杀入账(首杀档位 + 累计逐位)")
|
||||
check(resolve_ok, "点选恰兑付选中一件(值=表值;未选项零生效)")
|
||||
var seq: Array = st["encounter_max_hp_seq"]
|
||||
var k := 5.0 # hp+400 ⇒ TunePanel 百分比后乘 ×5(加算先加、百分比后乘——生效次序口径)
|
||||
var exp_base := maxi(1, int(round(float(int(_db.hero.max_hp)) * k)))
|
||||
var valid_hp := {}
|
||||
for acc in 4:
|
||||
valid_hp[maxi(1, int(round(float(int(_db.hero.max_hp) + acc * max_flat) * k)))] = true
|
||||
# 模拟档 v3.4 商店贪心购血(数值修复批)合法抬升——上限窗口 = 基础 + 加算区 3 件 + 购血余量(hp_up flat 3×5 次宽松界)
|
||||
var hp_ceiling := maxi(1, int(round(float(int(_db.hero.max_hp) + 3 * max_flat + 15) * k)))
|
||||
var seq_ok := seq.size() > 0 and int(seq[0]) == exp_base
|
||||
for v in (seq as Array):
|
||||
if not valid_hp.has(int(v)):
|
||||
if int(v) < exp_base or int(v) > hp_ceiling:
|
||||
seq_ok = false
|
||||
check(seq_ok,
|
||||
"各战上限入参 ∈ {基础..基础+3×件值}(覆盖值通道——通关+奖励节点双渠道共享上限 3;首战=基础 %d)" % exp_base)
|
||||
"各战上限入参 ∈ [基础, 基础+3×件值+购血余量](覆盖值通道——双渠道共享上限 3;模拟档贪心购血合法叠加〔数值修复批 v3.4〕;首战=基础 %d)" % exp_base)
|
||||
# ③ 种子域可复现(同种子两局:summary + 上限序列 + 行进序列 + 三选一停点载荷逐位一致)
|
||||
var st2: Dictionary = autoplay.run_adventure(_db, 42, 900, 400)
|
||||
check(st2["summary"] == sum and st2["encounter_max_hp_seq"] == seq
|
||||
@@ -3391,7 +3393,7 @@ func _smoke_nine_types_gp_stream() -> void:
|
||||
gp_neg = int(row.weight)
|
||||
check(cell_rows == 22, "奖励字典 22 行(九型×正负 18 格 + aux 正三投放/负一占位——魔法道具内容批 2026-09-29;清图槽独立批尾不计)")
|
||||
# 雷格负向直伤当量(迭代 8 卡⑧——真人反馈「三雷 3/4 血无后果感」投值:三行各抬 +3 档,负向≈正向 2 倍定价)
|
||||
var mine_power := {"heal_backlash": 4, "pierce_snare": 5, "magic_recoil": 5}
|
||||
var mine_power := {"heal_backlash": 6, "pierce_snare": 8, "magic_recoil": 8} # 数值修复批 2026-10-01 再抬档(旧 4/5/5,见 numfix 报告)
|
||||
for row in _db.reward.rows:
|
||||
var rid := String(row.get("reward_id", ""))
|
||||
if mine_power.has(rid):
|
||||
|
||||
Reference in new issue
Block a user