balance(m3-iter5-T4): 敌方AC行值投值·Boss-only终版(杂鱼全0+Boss台阶8/9/12)——v1硬梯度14.3%/v2软15.7%/v3 Boss-only 17.1%三版穷尽证普通L2与AC不可兼得,17.1%挂账并列U2追认(与完美L1丙同形态);生存链复算=判定式阈值AC+EV表与表值锚逐位吻合(暴击相消口径)+折扣档被damage_floor压平为1点悬崖;L2池复验过(杂鱼93%健康/Boss 83%·9.4轮税收精准);冒烟占位断言(全行AC=0)换新不变量守带宽+台阶,557→558只增

This commit is contained in:
DKChan committed 2026-09-28 23:09:03 +08:00
1 parent 09dd580447
commit 8ffc5552f1
10 files changed
+1429 -28

No files matched your search

+4 -3
View File
@@ -110,7 +110,7 @@ rows = [{
"weight": 2
}],
"armor_type": "light",
"armor_class": 0
"armor_class": 8
}, {
"level_index": 2,
"node_depth": 0,
@@ -270,7 +270,7 @@ rows = [{
"weight": 2
}],
"armor_type": "ward",
"armor_class": 0
"armor_class": 9
}, {
"level_index": 3,
"node_depth": 0,
@@ -427,7 +427,7 @@ rows = [{
"weight": 2
}],
"armor_type": "heavy",
"armor_class": 0
"armor_class": 12
}]
; 切片调参登记 v3(2026-09-12 试玩批次 1 触发——F7 流程;C3 GDD F5 对照):
@@ -562,3 +562,4 @@ rows = [{
; M-3 迭代 5 判读锁值批(2026-09-28):(2,9) 卓尔 hp 130→100(杠杆扫描 C3 组合——Boss 杠杆对普通档 +4pp;
; r_boss 锚漂移加重 100/389≈0.26 低于域 [0.5,1.1] 下限,收锚裁决挂迭代 5 出口人闸;读数见
; design/qa/m3-iter5-lever-scan-2026-09-28.md)。
; M-3 迭代 5 T4 AC 投值批·终版 Boss-only(2026-09-28:v1 硬梯度普通 L2 14.3% 越带/v2 软梯度 15.7% 仍越带——杂鱼税对普通画像〔普通分量为主力输出〕系统性超压;终版=杂鱼全 0〔带 0~5 下缘,掷点戏剧性集中 Boss 战〕+Boss 台阶 8/9/12 严格递增;三版演化链读数见 design/qa/m3-iter5-t4-ac-2026-09-28.md,随出口人闸追认)。
+52 -24
View File
@@ -135,7 +135,7 @@ func _smoke_adventure_api() -> void:
adr.level_started.connect(_api_mark_started.bind(sig_seq))
adr.choices_presented.connect(_api_mark_choices.bind(sig_seq, obs))
adr.node_entered.connect(_api_mark_node.bind(sig_seq))
check(adr.start_adventure(42), "冒险启动受理(主种子注入恰一次——state=running)")
check(adr.start_adventure(42, {"first_script": true}), "冒险启动受理(主种子注入恰一次——state=running;first_script=固定模板 0——精确路径断言需要现行图,零抽签)")
check(not adr.start_adventure(43), "进行中重复 start_adventure 拒绝(重开须待结算完毕)")
check(sig_seq.size() == 2 and String(sig_seq[0]) == "level_started" and String(sig_seq[1]) == "choices_presented",
"停点前移次序契约:level_started 先于入口候选呈现(反序选路口会被呈现层 runner 空防御丢弃)")
@@ -193,7 +193,7 @@ func _smoke_adventure() -> void:
en_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true
var miss := 0
for row in _db.level.rows:
for n in row.nodes:
for n in _level_active_nodes(int(row.level_index)):
var nt := String(n.get("type", ""))
if nt != "battle" and nt != "boss":
continue
@@ -275,7 +275,7 @@ func _smoke_adventure() -> void:
# ⑥ 重复宣布拒绝(settle 后再调 _on_level_finished——摘要零变化)
var adr6 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(7))
adr6.set_hero_override(900, 400)
check(adr6.start_adventure(7), "重复拒绝场景:冒险启动(seed 7)")
check(adr6.start_adventure(7, {"first_script": true}), "重复拒绝场景:冒险启动(seed 7——固定模板 0 同上)")
adr6.runner().pick_next_node(int(adr6.runner().next_candidate_ids()[0])) # 停点前移批 2026-09-23:入口停点 pick 进首战
var b6 = adr6.runner().current_battle()
var st6 := _adv_st()
@@ -307,7 +307,7 @@ func _smoke_adventure() -> void:
for s in range(42, 92):
var a8 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(s))
a8.set_hero_override(900, 400)
if not a8.start_adventure(s):
if not a8.start_adventure(s, {"first_script": true}): # 固定模板 0——种子扫描语义按现行图
continue
if _adv_drive_to_shop(a8, _adv_st(), autoplay):
same = {"a": a8, "s": s}
@@ -347,7 +347,7 @@ func _smoke_adventure() -> void:
# ⑩ 放弃(战斗中拒绝 · 非战斗期受理同失败型结算)
var a10 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(15))
a10.set_hero_override(900, 400)
check(a10.start_adventure(15), "放弃场景:冒险启动(seed 15)")
check(a10.start_adventure(15, {"first_script": true}), "放弃场景:冒险启动(seed 15——固定模板 0 同上)")
check(a10.runner().pick_next_node(int(a10.runner().next_candidate_ids()[0])),
"放弃场景:入口停点 pick 进首战(停点前移批 2026-09-23 驱动口径)")
check(not a10.abandon(), "战斗中 abandon 拒绝(战斗域单事务在飞)")
@@ -529,8 +529,8 @@ func _smoke_level() -> void:
check(lose.result == "defeat", "关卡·裸装(atk≈1/hp≈10)败北路径可达(实测:%s)" % lose.result)
check(lose.defeat_node_index >= 0 and lose.defeat_node_index < 7, "败北节点记录在域内(节点 %d)" % lose.defeat_node_index)
# ④ 回放确定性:同种子两遍步骤流逐行相同
var rep1 = autoplay.run_level(_db, 777, 1, 100, 100, true)
var rep2 = autoplay.run_level(_db, 777, 1, 100, 100, true)
var rep1 = autoplay.run_level(_db, 777, 1, 100, 100, true, null, "normal", "min", 0)
var rep2 = autoplay.run_level(_db, 777, 1, 100, 100, true, null, "normal", "min", 0)
check(JSON.stringify(rep1.rows) == JSON.stringify(rep2.rows), "关卡回放断言:同种子两遍结算步骤流逐行相同")
check(rep1.rows.size() > 0, "关卡步骤流非空(%d 行)" % rep1.rows.size())
# ⑤ 属性覆盖恢复:模拟后 hero 表回基线(调整板不泄漏到后续会话;基线 = 函数入口的配置快照,改表自动跟随)
@@ -582,11 +582,28 @@ func _smoke_ac_swap_guard() -> void:
# ③ 单位表 armor_class:域内校验(配置推导——正式投值归怪物/难度批,改表不断锚)
check(int(_db.hero.armor_class) >= 0 and int(_db.hero.armor_class) <= 30,
"主角 armor_class 在域内(%d——暂定基准随表走)" % int(_db.hero.armor_class))
var ac_all := true
var ac_band := true
var ac_detail := ""
for row in _db.enemy.rows:
if not row.has("armor_class") or int(row.armor_class) != 0:
ac_all = false
check(ac_all, "敌方全行 armor_class = 0(首关暂定——正式行值归怪物设定批待办)")
var ac: int = int(row.get("armor_class", -1))
var boss_row: bool = String(row.get("enemy_id", "")).ends_with("_chief") or \
String(row.get("enemy_id", "")) == "drider" or String(row.get("enemy_id", "")) == "duergar_king"
# 精英编码位现行不存在(registry/敌人系统:精英 P2 内容承载)——非 Boss 行全按杂鱼带 0~5
var lo := 8 if boss_row else 0
var hi := 12 if boss_row else 5
if ac < lo or ac > hi:
ac_band = false
ac_detail += " %s:%d" % [String(row.get("display_name", "?")), ac]
check(ac_band, "敌方 AC 行值在已裁带宽内(杂鱼 0~5·Boss 8~12——M-3 迭代 5 T4 投值批,越带行%s)" % [
ac_detail if ac_detail != "" else "无"])
# ③b Boss 台阶断言(关卡即台阶——L1 8 < L2 10 < L3 12)+ 首遇位教学位低 AC(≤3)
var boss_ac := {}
for row in _db.enemy.rows:
var eid := String(row.get("enemy_id", ""))
if eid == "ogre_chief" or eid == "drider" or eid == "duergar_king":
boss_ac[int(row.get("level_index", 0))] = int(row.get("armor_class", 0))
check(int(boss_ac.get(1, 0)) < int(boss_ac.get(2, 0)) and int(boss_ac.get(2, 0)) < int(boss_ac.get(3, 0)),
"Boss AC 关卡台阶严格递增(%d < %d < %d)" % [int(boss_ac.get(1, 0)), int(boss_ac.get(2, 0)), int(boss_ac.get(3, 0))])
# ④ 掷点参数域(加载校验已过——此处钉形状语义)
var bounds: Array = _db.settlement.roll_tier_bounds
var discounts: Array = _db.settlement.roll_tier_discounts
@@ -1565,11 +1582,22 @@ func _smoke_encounter_roll() -> void:
check(scan_bad == 0, "全深度 × 12 种子 spawn 全域内(%d 场,∈[unit_count, unit_count_max] 且 ≥1)——空遭遇不可达(Bug#1 回归)" % scan_total)
# 模板感知取节点表(M-3 迭代 5 T5——row.templates 非空取模板 0=现行固定图,保持既有断言语义)
func _level_active_nodes(level_index: int) -> Array:
for row in _db.level.rows:
if int(row.level_index) == level_index:
var tpls: Array = row.get("templates", [])
if not tpls.is_empty():
return tpls[0].get("nodes", [])
return row.get("nodes", [])
return []
func _level_battle_nodes(level_index: int) -> Array:
var out: Array = []
for row in _db.level.rows:
if int(row.level_index) == level_index:
for node in row.nodes:
for node in _level_active_nodes(level_index):
var t := String(node.get("type", ""))
if t == "battle" or t == "boss":
out.append(node)
@@ -2553,10 +2581,10 @@ func _smoke_level_graph() -> void:
for row in _db.level.rows:
if int(row.level_index) == 2:
l2 = row
check(not l2.is_empty() and l2.nodes.size() == 10, "L2 节点数 = 10(入口 1 + 中间三层 2/3/2 + 商店房 + Boss——商店房批 Boss 重编号 8→9)")
check(not l2.is_empty() and _level_active_nodes(2).size() == 10, "L2 节点数 = 10(入口 1 + 中间三层 2/3/2 + 商店房 + Boss——商店房批 Boss 重编号 8→9)")
var tcount := {"battle": 0, "reward": 0, "rest": 0, "boss": 0, "shop": 0}
var branch := 0
for node in l2.nodes:
for node in _level_active_nodes(2):
tcount[String(node.get("type", ""))] = int(tcount[String(node.get("type", ""))]) + 1
if node.get("next", []).size() >= 2:
branch += 1
@@ -2599,7 +2627,7 @@ func _smoke_level_graph() -> void:
check(_has_err_category(orphan_errs, "structural"), "图校验拒绝:孤儿节点(从入口不可达 / 双入口同体违例)")
# ④ L2 选路推进:min / max 双路径全通(确定性路径轴——零随机流自开)
# 并流批(ADR-0007 裁丙)后战斗随机值面漂移——max 路径种子改行为面扫描(路线形状/商店/选路口为断言本体)
var win_min = autoplay.run_level(_db, 42, 2, 200, 100)
var win_min = autoplay.run_level(_db, 42, 2, 200, 100, false, null, "normal", "min", 0)
check(win_min.result == "victory", "L2·满配 min 路径通关(实测:%s)" % win_min.result)
check(win_min.boss_depth == 9, "L2 Boss 以编号 9 解析(终点编号最大——商店房批重编号 8→9)")
check(win_min.visited_ids == [0, 1, 3, 6, 8, 9], "min 路径 = 0→1→3(恢复)→6→8(商店零购离开)→9(实测 %s)" % str(win_min.visited_ids))
@@ -2607,7 +2635,7 @@ func _smoke_level_graph() -> void:
var win_max := {}
var max_seed := -1
for s in range(42, 300):
win_max = autoplay.run_level(_db, s, 2, 200, 100, false, null, "normal", "max")
win_max = autoplay.run_level(_db, s, 2, 200, 100, false, null, "normal", "max", 0)
if win_max.result == "victory" and win_max.visited_ids == [0, 2, 5, 7, 8, 9]:
max_seed = s
break
@@ -2618,7 +2646,7 @@ func _smoke_level_graph() -> void:
runner.set_hero_override(200, 100)
var choice_sizes: Array = []
runner.choices_presented.connect(func(c): choice_sizes.append(c.size()))
check(runner.start_level(2, 42), "手动驱动:L2 启动(入口战斗直接开打——入口无前置兑付)")
check(runner.start_level(2, 42, {}, 0), "手动驱动:L2 启动(入口战斗直接开打——入口无前置兑付;template_pick=0 固定现行图——精确选路断言)")
var st = {"result": "", "rounds": 0, "refreshes": 0, "mine_hits": 0, "hero_dmg": 0, "policy": "normal"}
var first = runner.current_battle()
var res_box := {} # 信号写回容器(lambda 按值捕获局部变量——Dictionary 引用可变)
@@ -2671,7 +2699,7 @@ func _smoke_level_graph() -> void:
for row in _db.enemy.rows:
en_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true
var cross_ok := true
for node in l2.nodes:
for node in _level_active_nodes(2):
var t := String(node.get("type", ""))
if t != "battle" and t != "boss":
continue
@@ -2690,10 +2718,10 @@ func _smoke_level_graph() -> void:
for row in _db.level.rows:
if int(row.level_index) == 3:
l3 = row
check(not l3.is_empty() and l3.nodes.size() == 10, "L3 节点数 = 10(入口 1 + 中间三层 2/3/2 + 商店房 + Boss——L2 同模板 + 商店房批)")
check(not l3.is_empty() and _level_active_nodes(3).size() == 10, "L3 节点数 = 10(入口 1 + 中间三层 2/3/2 + 商店房 + Boss——L2 同模板 + 商店房批)")
var tcount3 := {"battle": 0, "reward": 0, "rest": 0, "boss": 0, "shop": 0}
var branch3 := 0
for node in l3.nodes:
for node in _level_active_nodes(3):
tcount3[String(node.get("type", ""))] = int(tcount3[String(node.get("type", ""))]) + 1
if node.get("next", []).size() >= 2:
branch3 += 1
@@ -2702,7 +2730,7 @@ func _smoke_level_graph() -> void:
check(branch3 == 4, "L3 真实岔路 = %d 处(0→[1,2] · 1→[3,4] · 2→[4,5] · 4→[6,7])" % branch3)
# ⑨ 跨表行完整:L3 每战斗/Boss 节点 (3, 编号) 在 board_gen 与 enemy 精确行(⑦ 的 bg_keys/en_keys 复用)
var cross_ok3 := true
for node in l3.nodes:
for node in _level_active_nodes(3):
var t3 := String(node.get("type", ""))
if t3 != "battle" and t3 != "boss":
continue
@@ -2934,7 +2962,7 @@ func _smoke_shop_room() -> void:
for s in range(42, 92):
var runner = load("res://core/level_runner.gd").new(_db, _fresh_rng(s))
runner.set_hero_override(900, 400) # 行进段快速通关档(购买断言与百分比轴正交——加算区单独断言)
runner.start_level(2, s)
runner.start_level(2, s, {}, 0) # 固定模板 0——商店经济断言按现行图 GP 账本
var st := {"result": "", "rounds": 0, "refreshes": 0, "mine_hits": 0, "hero_dmg": 0, "policy": "normal"}
var guard := 40
while String(runner.level_result) == "" and int(runner.current_node_id()) != 8 and guard > 0:
@@ -3055,7 +3083,7 @@ func _smoke_shop_graph() -> void:
var shops: Array = []
var boss_id := -1
var max_id := -1
for n in row.nodes:
for n in _level_active_nodes(L):
var t := String(n.get("type", ""))
if t == "shop":
shops.append(int(n.get("node_depth", -1)))
@@ -3064,7 +3092,7 @@ func _smoke_shop_graph() -> void:
max_id = maxi(max_id, int(n.get("node_depth", -1)))
check(shops.size() == 1 and int(shops[0]) == 8, "L%d 商店房恰 1 且 node_depth=8(next=[9])" % L)
check(boss_id == 9 and boss_id == max_id, "L%d Boss = 9 = 全图最大编号(商店房批重编号 8→9)" % L)
var paths: Array = _all_entry_boss_paths(row.nodes)
var paths: Array = _all_entry_boss_paths(_level_active_nodes(L))
var via_shop := true
for p in paths:
if not p.has(8):