初始展开收窄批T0-T4(迭代14①):甲=剃雷域收窄(切比雪夫≤1+r 参数化 shave_ring_radius,r=1→5×5=25格,全域flood剃雷退役——零RNG消费变化同种子布雷逐位一致)+乙=R₀重掷闸(合法性闸序列 r0_cap_exceeded+同流重试+耗尽走三分出口accept_worst诚实降级+metrics r0_cap_rejected)+参数面(schema两列域校验[0,2]/[9,半板]加载期致命+19行投值)+calibrate扩r0方差/分位/闸触发列+参数透传;S1停机呈报已裁(用户方案A:10×10 cap 30→45——r=1自然R₀均值39>30锚结构性倒挂94%拒绝常态非法板,P1预案字面上调;12×12/13×13 43/51健康侧维持);断言面=+12(R₀≤cap结构/r0_cap_rejected在位/ring_removed≤剃雷域候选/cap压线9诚实降级/确定性两遍逐位)+夹具复核3处(relic候选身份kind|tier|relic_id按TP0 v2.2、商店购买笔数推导防GP流漂移);闸=冒烟767/0(755+12)

This commit is contained in:
DKChan committed 2026-10-05 08:19:41 +08:00
1 parent 3e2c564b59
commit 30cabbdf52
5 files changed
+160 -32

No files matched your search

+27 -7
View File
@@ -30,6 +30,10 @@ func generate(L: int, D: int, rng_stream: RandomNumberGenerator, db) -> BoardDat
"min_safe": int(row.min_safe_count),
"max_attempts": int(row.max_gen_attempts),
"board_type": String(row.board_type),
# 初始展开收窄批(迭代14① T1——两新参数;.get 缺省=legacy 调用方防御口径:shave_r=1 现行投值、
# r0_cap=0 闸关。正式表行全行投值,加载期校验在 BoardGenTable.validate)
"shave_r": int(row.get("shave_ring_radius", 1)),
"r0_cap": int(row.get("r0_max_cap", 0)),
}
var best_legal: BoardData = null
var best_legal_score := -1.0
@@ -38,6 +42,7 @@ func generate(L: int, D: int, rng_stream: RandomNumberGenerator, db) -> BoardDat
var attempts_used := 0
var accepted: BoardData = null
var accept_mode := ""
var r0_cap_rejected := 0
for k in range(1, params.max_attempts + 1):
attempts_used = k
var b := _attempt(params, rng_stream, db)
@@ -45,6 +50,9 @@ func generate(L: int, D: int, rng_stream: RandomNumberGenerator, db) -> BoardDat
if best_bad == null or _lex_less(badness, best_badness):
best_bad = b
best_badness = badness
# R₀≤cap 重掷闸触发计数(迭代14① T3——尝试事实,与合法性判定独立记录;占比=T5 闸触发率读数)
if int(params.r0_cap) > 0 and b.compute_r0().size() > int(params.r0_cap):
r0_cap_rejected += 1
if legality(b, params, db):
var m: Dictionary = measure_inferability(b) # 求解器每合法板恰一次(O3 收口——此前重复调用两次)
var inf: float = m["inferability"]
@@ -66,7 +74,7 @@ func generate(L: int, D: int, rng_stream: RandomNumberGenerator, db) -> BoardDat
accept_mode = "accept_worst"
accepted.illegal_product = true
push_error("[棋盘生成] attempts 耗尽零合法——接受最不坏出图 + 非法产物标记(玩家永不卡死)")
_finalize(accepted, params, attempts_used, accept_mode, db)
_finalize(accepted, params, attempts_used, accept_mode, db, r0_cap_rejected)
return accepted
@@ -99,12 +107,16 @@ func _attempt(params: Dictionary, rng_stream: RandomNumberGenerator, db) -> Boar
for i in range(0, mini(n_mines, domain.size())):
b.mines[domain[i]] = true
_compute_digits(b)
# 先布后删:0 格闭包(布后基准)外扩 1 环内的雷移除(只减不增、无迭代)
# 先布后删:剃雷域 = 距 spawn_center 切比雪夫距离 ≤1+r 的格(迭代14①收窄批 T2——原「R₀ flood 域∪
# 外扩 1 环」全域剃雷=开局开阔地巨大根因〔10×10 板 R₀ 均值 68.6=板面 68.6%〕;收窄后开阔地=
# 出生邻域外的自然 flood。r 读表 shave_ring_radius(r=1 → 5×5=25 格;r=0 即只剃出生 3×3)。
# 只减不增、无迭代语义不变;ring_removed 指标语义不变(现值变小=预期);零 RNG 消费变化——
# 同种子布雷序列逐位一致,仅移除域收窄。
var shave_r: int = int(params.get("shave_r", 1))
var ring := {}
for c in b.compute_r0():
for n in b.neighbors8(c):
if not ring.has(n):
ring[n] = true
for c in b.all_cells():
if maxi(absi(c.x - b.spawn_center.x), absi(c.y - b.spawn_center.y)) <= 1 + shave_r:
ring[c] = true
var removed := 0
for c in ring.keys():
if b.mines.has(c):
@@ -300,6 +312,12 @@ func _legality_faults(b: BoardData, params: Dictionary) -> Array:
var min_safe: int = int(params.get("min_safe", params.get("min_safe_count", 0)))
if b.safe_cells().size() < min_safe:
faults.append("min_safe_short")
# ⑤ R₀≤cap 重掷闸(迭代14①收窄批 T3——合法性先于质量评分,同 inv4 白给板先例;超阈整板作废
# 同流重试〔重试共用 board_gen 流,attempt 边界非 scope 事件〕;全部尝试超阈 → 既有三分出口
# accept_worst 诚实降级+非法产物标记(玩家永不卡死)。cap=0 = 闸关(legacy 调用方防御口径)
var r0_cap: int = int(params.get("r0_cap", 0))
if r0_cap > 0 and r0.size() > r0_cap:
faults.append("r0_cap_exceeded")
return faults
@@ -313,7 +331,7 @@ func _neighbor_mine_count(b: BoardData, c: Vector2i) -> int:
# ==================== 产出上报(生成期指标集) ====================
func _finalize(b: BoardData, params: Dictionary, attempts_used: int, accept_mode: String, _db) -> void:
func _finalize(b: BoardData, params: Dictionary, attempts_used: int, accept_mode: String, _db, r0_cap_rejected: int = 0) -> void:
var m := measure_inferability(b)
# 产物断言(棋盘生成 出生区安全与反退化布雷):实际雷数 ≤ 目标雷数 N——先布后删只减不增,失败即致命
var n_target: int = int(round(float(params.density) * float(params.w * params.h - BoardData.SPAWN_AREA_CELLS)))
@@ -332,6 +350,8 @@ func _finalize(b: BoardData, params: Dictionary, attempts_used: int, accept_mode
"illegal_product": accept_mode == "accept_worst",
"ring_removed": attempt_facts.get("ring_removed", 0),
"r0_count": m["r0"],
# R₀≤cap 重掷闸尝试事实(迭代14① T3——占比=T5 闸触发率读数,过高=甲乙配比失衡信号)
"r0_cap_rejected": r0_cap_rejected,
}
b.illegal_product = accept_mode == "accept_worst"
b.degraded_product = accept_mode == "accept_best"
+11
View File
@@ -65,6 +65,17 @@ func validate() -> Array:
var bt: String = str(row.get("board_type", ""))
if not BOARD_TYPE_VOCABULARY.has(bt):
errs.append({"category": "cross_field", "message": "(%d,%d) board_type 越词表: %s" % [L, D, bt]})
# 初始展开收窄批两参数(迭代14① T1——加载期校验违例致命;缺省 shave=1 现行投值 / cap=0 闸关防御口径)
var sr: int = int(row.get("shave_ring_radius", 1))
if sr < 0 or sr > 2:
errs.append({"category": "domain", "message": "(%d,%d) shave_ring_radius 越域 [0,2]: %d" % [L, D, sr]})
var r0_cap: int = int(row.get("r0_max_cap", 0))
if r0_cap > 0:
# 护栏(任务书 §二):cap ≥ 出生格数 9(BoardData.SPAWN_AREA_CELLS)、cap ≤ 板格数一半
if r0_cap < BoardData.SPAWN_AREA_CELLS:
errs.append({"category": "cross_field", "message": "(%d,%d) r0_max_cap 低于出生保障面 %d: %d" % [L, D, BoardData.SPAWN_AREA_CELLS, r0_cap]})
if r0_cap > (w * h) / 2:
errs.append({"category": "cross_field", "message": "(%d,%d) r0_max_cap 超板格数一半 %d: %d" % [L, D, (w * h) / 2, r0_cap]})
anchor_ok[L] = anchor_ok.get(L, false) or D == 0
for L in anchor_ok.keys():
if not anchor_ok[L]:
+57 -19
View File
@@ -14,7 +14,9 @@ rows = [{
"solver_rules": "single_subset",
"min_safe_count": 50,
"max_gen_attempts": 16,
"board_type": "dungeon"
"board_type": "dungeon",
"shave_ring_radius": 1,
"r0_max_cap": 45
}, {
"level_index": 1,
"node_depth": 1,
@@ -25,7 +27,9 @@ rows = [{
"solver_rules": "single_subset",
"min_safe_count": 50,
"max_gen_attempts": 16,
"board_type": "dungeon"
"board_type": "dungeon",
"shave_ring_radius": 1,
"r0_max_cap": 45
}, {
"level_index": 1,
"node_depth": 2,
@@ -36,7 +40,9 @@ rows = [{
"solver_rules": "single_subset",
"min_safe_count": 50,
"max_gen_attempts": 16,
"board_type": "dungeon"
"board_type": "dungeon",
"shave_ring_radius": 1,
"r0_max_cap": 45
}, {
"level_index": 1,
"node_depth": 3,
@@ -47,7 +53,9 @@ rows = [{
"solver_rules": "single_subset",
"min_safe_count": 50,
"max_gen_attempts": 16,
"board_type": "dungeon"
"board_type": "dungeon",
"shave_ring_radius": 1,
"r0_max_cap": 45
}, {
"level_index": 1,
"node_depth": 4,
@@ -58,7 +66,9 @@ rows = [{
"solver_rules": "single_subset",
"min_safe_count": 60,
"max_gen_attempts": 24,
"board_type": "dungeon"
"board_type": "dungeon",
"shave_ring_radius": 1,
"r0_max_cap": 43
}, {
"level_index": 2,
"node_depth": 0,
@@ -69,7 +79,9 @@ rows = [{
"solver_rules": "single_subset",
"min_safe_count": 60,
"max_gen_attempts": 24,
"board_type": "underdark"
"board_type": "underdark",
"shave_ring_radius": 1,
"r0_max_cap": 43
}, {
"level_index": 2,
"node_depth": 1,
@@ -80,7 +92,9 @@ rows = [{
"solver_rules": "single_subset",
"min_safe_count": 60,
"max_gen_attempts": 24,
"board_type": "underdark"
"board_type": "underdark",
"shave_ring_radius": 1,
"r0_max_cap": 43
}, {
"level_index": 2,
"node_depth": 4,
@@ -91,7 +105,9 @@ rows = [{
"solver_rules": "single_subset",
"min_safe_count": 60,
"max_gen_attempts": 24,
"board_type": "underdark"
"board_type": "underdark",
"shave_ring_radius": 1,
"r0_max_cap": 43
}, {
"level_index": 2,
"node_depth": 5,
@@ -102,7 +118,9 @@ rows = [{
"solver_rules": "single_subset",
"min_safe_count": 60,
"max_gen_attempts": 24,
"board_type": "underdark"
"board_type": "underdark",
"shave_ring_radius": 1,
"r0_max_cap": 43
}, {
"level_index": 2,
"node_depth": 6,
@@ -113,7 +131,9 @@ rows = [{
"solver_rules": "single_subset",
"min_safe_count": 60,
"max_gen_attempts": 24,
"board_type": "underdark"
"board_type": "underdark",
"shave_ring_radius": 1,
"r0_max_cap": 43
}, {
"level_index": 2,
"node_depth": 7,
@@ -124,7 +144,9 @@ rows = [{
"solver_rules": "single_subset",
"min_safe_count": 60,
"max_gen_attempts": 24,
"board_type": "underdark"
"board_type": "underdark",
"shave_ring_radius": 1,
"r0_max_cap": 43
}, {
"level_index": 2,
"node_depth": 9,
@@ -135,7 +157,9 @@ rows = [{
"solver_rules": "single_subset",
"min_safe_count": 60,
"max_gen_attempts": 24,
"board_type": "underdark"
"board_type": "underdark",
"shave_ring_radius": 1,
"r0_max_cap": 43
}, {
"level_index": 3,
"node_depth": 0,
@@ -146,7 +170,9 @@ rows = [{
"solver_rules": "single_subset",
"min_safe_count": 70,
"max_gen_attempts": 32,
"board_type": "deep_underdark"
"board_type": "deep_underdark",
"shave_ring_radius": 1,
"r0_max_cap": 51
}, {
"level_index": 3,
"node_depth": 1,
@@ -157,7 +183,9 @@ rows = [{
"solver_rules": "single_subset",
"min_safe_count": 70,
"max_gen_attempts": 32,
"board_type": "deep_underdark"
"board_type": "deep_underdark",
"shave_ring_radius": 1,
"r0_max_cap": 51
}, {
"level_index": 3,
"node_depth": 4,
@@ -168,7 +196,9 @@ rows = [{
"solver_rules": "single_subset",
"min_safe_count": 70,
"max_gen_attempts": 32,
"board_type": "deep_underdark"
"board_type": "deep_underdark",
"shave_ring_radius": 1,
"r0_max_cap": 51
}, {
"level_index": 3,
"node_depth": 5,
@@ -179,7 +209,9 @@ rows = [{
"solver_rules": "single_subset",
"min_safe_count": 70,
"max_gen_attempts": 32,
"board_type": "deep_underdark"
"board_type": "deep_underdark",
"shave_ring_radius": 1,
"r0_max_cap": 51
}, {
"level_index": 3,
"node_depth": 6,
@@ -190,7 +222,9 @@ rows = [{
"solver_rules": "single_subset",
"min_safe_count": 70,
"max_gen_attempts": 32,
"board_type": "deep_underdark"
"board_type": "deep_underdark",
"shave_ring_radius": 1,
"r0_max_cap": 51
}, {
"level_index": 3,
"node_depth": 7,
@@ -201,7 +235,9 @@ rows = [{
"solver_rules": "single_subset",
"min_safe_count": 70,
"max_gen_attempts": 32,
"board_type": "deep_underdark"
"board_type": "deep_underdark",
"shave_ring_radius": 1,
"r0_max_cap": 51
}, {
"level_index": 3,
"node_depth": 9,
@@ -212,7 +248,9 @@ rows = [{
"solver_rules": "single_subset",
"min_safe_count": 82,
"max_gen_attempts": 32,
"board_type": "deep_underdark"
"board_type": "deep_underdark",
"shave_ring_radius": 1,
"r0_max_cap": 59
}]
; 切片调参登记(SLICE-TUNED provisional,调参所有权仍归 C3):
+29 -3
View File
@@ -28,7 +28,7 @@ func _initialize() -> void:
quit(1)
return
var gen = load("res://core/board_generator.gd").new()
print("level,depth,w,h,n,mines_mean,mines_med,safe_mean,safe_med,r0_mean,inf_mean")
print("level,depth,w,h,n,mines_mean,mines_med,safe_mean,safe_med,r0_mean,r0_sd,r0_p90,r0_max,inf_mean,cap_rej_mean")
for row in db.board_gen.rows:
var rd := {
"level_index": int(row.level_index), "node_depth": int(row.node_depth),
@@ -36,6 +36,9 @@ func _initialize() -> void:
"mine_density": float(row.mine_density), "inferability_target": float(row.inferability_target),
"solver_rules": String(row.solver_rules), "min_safe_count": int(row.min_safe_count),
"max_gen_attempts": int(row.max_gen_attempts), "board_type": String(row.board_type),
# 初始展开收窄批(迭代14①)两新参数透传(.get 缺省与生成器防御口径一致)
"shave_ring_radius": int(row.get("shave_ring_radius", 1)),
"r0_max_cap": int(row.get("r0_max_cap", 0)),
}
var fdb := FakeDB.new()
fdb.row = rd
@@ -43,6 +46,7 @@ func _initialize() -> void:
var safes: Array = []
var r0s: Array = []
var infs: Array = []
var cap_rejs: Array = []
for i in range(n):
var rg := RandomNumberGenerator.new()
rg.seed = 900000 + i
@@ -53,12 +57,14 @@ func _initialize() -> void:
safes.append(b.safe_cells().size())
r0s.append(int(b.metrics.r0_count))
infs.append(float(b.metrics.inferability))
cap_rejs.append(int(b.metrics.get("r0_cap_rejected", 0)))
if mines.is_empty():
printerr("L%d d%d 生成全部失败" % [int(row.level_index), int(row.node_depth)])
continue
print("%d,%d,%d,%d,%d,%.1f,%d,%.1f,%d,%.1f,%.3f" % [
print("%d,%d,%d,%d,%d,%.1f,%d,%.1f,%d,%.1f,%.2f,%d,%d,%.3f,%.2f" % [
int(row.level_index), int(row.node_depth), int(row.board_width), int(row.board_height),
mines.size(), _mean(mines), _med(mines), _mean(safes), _med(safes), _mean(r0s), _mean(infs)])
mines.size(), _mean(mines), _med(mines), _mean(safes), _med(safes), _mean(r0s),
_sd(r0s), _p90(r0s), _max(r0s), _mean(infs), _mean(cap_rejs)])
quit(0)
@@ -69,6 +75,26 @@ func _mean(a: Array) -> float:
return t / float(a.size())
func _sd(a: Array) -> float:
var m := _mean(a)
var t := 0.0
for v in a:
t += (float(v) - m) * (float(v) - m)
return sqrt(t / float(a.size()))
func _p90(a: Array) -> int:
var b := a.duplicate()
b.sort()
return int(b[mini(a.size() - 1, int(ceil(a.size() * 0.9)) - 1)])
func _max(a: Array) -> int:
var b := a.duplicate()
b.sort()
return int(b[b.size() - 1])
func _med(a: Array) -> int:
var b := a.duplicate()
b.sort()
+36 -3
View File
@@ -360,7 +360,9 @@ func _smoke_adventure() -> void:
cand_ok = false
var seen := {}
for c in cands:
var ckey := "%s|%s" % [String((c as Dictionary).get("kind", "")), String((c as Dictionary).get("tier", ""))]
# 身份键 = kind|tier|relic_id(迭代14① T4 断言基线复核——TP0 v2.2 relic 按 relic_id 分列
# 不参与同 kind 去重〔_token_identity relic: 前缀〕,同 tier 三枚不同流派件=三个不同选项合法态)
var ckey := "%s|%s|%s" % [String((c as Dictionary).get("kind", "")), String((c as Dictionary).get("tier", "")), String((c as Dictionary).get("relic_id", ""))]
if seen.has(ckey):
cand_ok = false
seen[ckey] = true
@@ -1253,6 +1255,24 @@ func _smoke_board_generator() -> void:
check(_board_dump(b2) == _board_dump(b3), "L%d 同种子两次生成逐格相同" % L)
var b4 = board_generator.generate(L, 0, _mk_stream("board_gen", 778), _db)
check(_board_dump(b2) != _board_dump(b4), "L%d 异种子产物不同(退化护栏)" % L)
# ---- 初始展开收窄批断言(迭代14① T4)----
# ① 结构断言:R₀ ≤ cap(正式表行投值,合法性闸承载——出图必过);② r0_cap_rejected 尝试事实在位;
# ③ ring_removed 量级:剃雷域收窄后 ≤ 域内候选雷格数(5×5−出生 9=16——全域剃雷退役);
# ④ 闸压线行为:cap=9(出生保障面下限)时出图必 r0≤9 或 accept_worst 非法标记(三分出口诚实降级,玩家永不卡死)
var cap_val: int = int(row.get("r0_max_cap", 0))
check(cap_val >= BoardData.SPAWN_AREA_CELLS, "L%d r0_max_cap 正式行投值在位(%d ≥ 出生保障面 9——板面 30%% 锚)" % [L, cap_val])
check(int(b1.metrics.r0_count) <= cap_val, "L%d 结构断言 R₀=%d ≤ cap=%d(R₀≤cap 重掷闸承载)" % [L, int(b1.metrics.r0_count), cap_val])
check(int(b1.metrics.get("r0_cap_rejected", -1)) >= 0, "L%d r0_cap_rejected 尝试事实指标在位(本图 %d 次触发)" % [L, int(b1.metrics.get("r0_cap_rejected", -1))])
check(int(b1.metrics.get("ring_removed", 99)) <= (2 + int(row.get("shave_ring_radius", 1))) * (2 + int(row.get("shave_ring_radius", 1))) * 4 - 5, "L%d ring_removed=%d ≤ 剃雷域候选雷格数((3+2r)²−9——全域剃雷退役)" % [L, int(b1.metrics.get("ring_removed", 99))])
var row9: Dictionary = row.duplicate()
row9["r0_max_cap"] = BoardData.SPAWN_AREA_CELLS
row9["max_gen_attempts"] = 4
var stub9 := _BoardRowStub.new()
stub9.row = row9
var b9 = board_generator.generate(1, 0, _mk_stream("board_gen", 909), stub9)
check(b9 != null and (int(b9.metrics.r0_count) <= BoardData.SPAWN_AREA_CELLS or b9.illegal_product),
"L1 cap 压线 9:出图 R₀=%d %s(三分出口诚实降级——超阈即 accept_worst 非法标记,玩家永不卡死)" % [int(b9.metrics.r0_count), "≤9" if int(b9.metrics.r0_count) <= BoardData.SPAWN_AREA_CELLS else ">9→illegal_product"])
check(int(b9.metrics.get("r0_cap_rejected", 0)) >= 0 and String(b9.metrics.get("accept_mode", "")) != "", "L1 cap 压线出图指标集完整(accept_mode=%s / 闸触发 %d 次)" % [String(b9.metrics.get("accept_mode", "")), int(b9.metrics.get("r0_cap_rejected", 0))])
func _mk_stream(stream_name: String, scope: int) -> RandomNumberGenerator:
@@ -1313,6 +1333,15 @@ class _CountVariantRng extends RngServiceCore:
return super.open_stream(purpose)
# 板行覆盖桩(迭代14① T4——cap 压线行为断言;接口面 = resolve_board_gen 单点)
class _BoardRowStub:
var row: Dictionary
func resolve_board_gen(_L: int, _D: int) -> Dictionary:
return row
func status_effect_row(_id: String) -> Dictionary:
return {}
func _smoke_global_variant() -> void:
print("--- 全局作用域变异 首发批(门控三维 + 替换制 + 资源面偏斜)---")
# ① 跨字段互锁负样本(resource_face ⟺ scope=global 三条 + effect_value 新域 [0,50])
@@ -3654,6 +3683,7 @@ func _smoke_shop_room() -> void:
check(runner.effective_ac() == int(_db.hero.armor_class) + 1, "AC = 10+1(armor_class 加算区轴,无百分比通道)")
# 余额不足购买被拒(零副作用):hp_up 连购至不可负担后兜底拒绝
var guard_drain := 20
var hp_buys := 1 # 夹具前段已购 1 笔(购买 hp_up 受理断言)——总数 = 前段 + 本循环
while guard_drain > 0:
guard_drain -= 1
var hp_price: int = int(_shop_products_by_id(runner.shop_products())["hp_up"]["price"])
@@ -3661,6 +3691,7 @@ func _smoke_shop_room() -> void:
break
if not runner.buy_product("hp_up"):
break
hp_buys += 1
var gp_dry: int = runner.gp
var tier_dry: int = runner.purchased_tier("hp_up")
var hp_dry: int = int(_shop_products_by_id(runner.shop_products())["hp_up"]["price"])
@@ -3673,8 +3704,10 @@ func _smoke_shop_room() -> void:
check(runner.effective_atk() == int(_db.hero.atk) + 1, "主角攻击生效值 = 2+1(基础表值 + 加算区)")
check(runner.pick_next_node(9) and runner.current_battle() != null, "选 Boss(节点 9)开战")
var snap: Dictionary = runner.current_battle().presentation_snapshot()
check(int(snap["hero"].get("atk", -1)) == int(_db.hero.atk) + 1 and int(snap["hero"].get("max_hp", -1)) == int(_db.hero.max_hp) + 3,
"战斗开始参数 = 基础表值 + 加算区(atk %d = 2+1 / max_hp %d = 17+3)" % [int(snap["hero"].get("atk", -1)), int(snap["hero"].get("max_hp", -1))])
# max_hp 期望按实际购买笔数推导(迭代14① T4 断言基线复核——棋盘换代改变战局 GP 流,可购笔数
# 随经济漂移;断言语义=「加算区笔数×3 全额入战斗开始参数」,不再锚死恰好 1 笔)
check(int(snap["hero"].get("atk", -1)) == int(_db.hero.atk) + 1 and int(snap["hero"].get("max_hp", -1)) == int(_db.hero.max_hp) + 3 * hp_buys,
"战斗开始参数 = 基础表值 + 加算区(atk %d = 2+1 / max_hp %d = %d+%d×3)" % [int(snap["hero"].get("atk", -1)), int(snap["hero"].get("max_hp", -1)), int(_db.hero.max_hp), hp_buys])
# 冒险收束(Boss 胜或败北——基线裸装口径任一结局)→ 钱包清零
var fin_end := {}
runner.level_finished.connect(func(p): fin_end["p"] = p)