换算率标定批:conversion 三行投值 6/8/1 + 护甲断链接线 + 判定端表值驱动(用户指令 2026-09-23)

- meta.tres conversion 三行 0→6/8/1(OFAT 注入档配对读数实测每级带平移 +6.5±0.7/+8.4±0.9/+1.4±0.4 点取整)
- 修复护甲断链:begin_battle hero_armor_class 参数零消费,_defender_ac 主角分支接线(缺参/-1=表值兜底,白板逐位不变)
- level_runner.start_level / run_level 增注入尾参(缺省 {} 零行为变更);verifier_batch 增 inject 档/levels 裁剪/armor_first 花费序/campaign 行 spend 标签
- balance_verifier.py:meta.tres 启动解析(表值锚配置推导化)+ 带平移判定 + ④标定段(配对差/逆方差合并/建议投值)
- 元进度 v1.4(换算率节回填+P1 销项)/ 06 删条 / registry 注记与变更记录
- 冒烟 553 项 0 失败;读数报告 production/qa/conversion-calibration/calibration.raw.md
This commit is contained in:
DKChan committed 2026-09-23 06:50:19 +08:00
1 parent 3309aae2a3
commit f0aca09bd0
13 files changed
+3943 -117

No files matched your search

@@ -643,10 +643,15 @@ func _roll_discount(roll: int, threshold: int) -> int:
# 防守方护甲等级读数(数值轴——与 armor_type 克制属性轴严格分离):敌人 = spawn 期行值快照;
# 主角 = 表值基准 + ac 型效果加算(ac_up 层数×幅度——不入任何池,无 clamp)
# 主角 = begin_battle 显式入参(换算率标定批 2026-09-23 接线——hero_armor_class 参数此前零消费,
# 永久强化护甲档与冒险加算区护甲轴的生效值在参数处断链、战斗只读表值——护甲行强条目重测暴露);
# 入参 = LevelRunner.effective_ac() 恰是 表值+base_override+bonus 合流,缺参/-1 = 表值兜底
# (冒烟/工具直呼路径逐位不变);ac 型状态效果叠其上(ac_up 层数×幅度——不入任何池,无 clamp)
func _defender_ac(unit: Dictionary) -> int:
if bool(unit.get("is_hero", false)):
var total: int = int(db.hero.armor_class)
var total: int = int(encounter.get("hero_armor_class", -1))
if total < 0:
total = int(db.hero.armor_class)
for eff in _status_effects.query_active("hero"):
var row: Dictionary = db.status_effect_row(eff.effect_id)
if not row.is_empty() and String(row.unit) == "ac":
@@ -110,9 +110,13 @@ func restore_hero_base() -> void:
# ---- 关卡生命周期 ----
func start_level(level_index: int, seed_val: int) -> bool:
func start_level(level_index: int, seed_val: int, base_override_in: Dictionary = {}) -> bool:
# 单关测试模式(旧路径——既有语义一字不改):私有账本、内置恢复数学、自调 new_adventure(漂移①
# 单关=冒险的历史简化在测试模式保留;正式种子域归 AdventureRunner.start_adventure)
# base_override_in = 永久强化 flat 注入尾参(换算率标定批 2026-09-23——缺省 {} 全零白板,
# 与既有调用方逐位一致零行为变更;契约同 AdventureRunner.start_adventure params.hero_override:
# 三键 {atk, max_hp, armor_class}、缺键/-1 = 0、负值 clamp 0。游戏直入口仍恒白板——
# 注入面只服务数值验证工具,非玩家可达路径)
_external = false
_rest_handler = Callable(self, "rest_heal_amount")
_ledger = {"hero_hp": 0, "gp": 0}
@@ -129,8 +133,10 @@ func start_level(level_index: int, seed_val: int) -> bool:
_graph_mode = false
gp = 0 # 冒险钱包清零开局(GP+商店房实施批——局内货币生命周期起点)
bonus = {"atk": 0, "max_hp": 0, "armor_class": 0} # 加算区冒险开始全 0
base_override = {"atk": 0, "max_hp": 0, "armor_class": 0} # 永久强化 flat 单关测试模式恒全零(白板——单关入口零元进度)
base_override = {"atk": 0, "max_hp": 0, "armor_class": 0} # 永久强化 flat:缺省全零白板(单关入口零元进度——注入面见上)
_shop_purchased = {} # 已购档数清零(跨冒险不延续——递增定价按冒险内累计)
for k in base_override.keys():
base_override[k] = maxi(0, int(base_override_in.get(k, 0))) # 注入规范化(契约同冒险开始参数面)
for node in nodes():
if node.has("next"):
_graph_mode = true
@@ -69,13 +69,13 @@ workshop_items = [{
}]
conversion = [{
"attr": "max_hp",
"per_level_shift_pct": 0
"per_level_shift_pct": 6
}, {
"attr": "atk",
"per_level_shift_pct": 0
"per_level_shift_pct": 8
}, {
"attr": "armor_class",
"per_level_shift_pct": 0
"per_level_shift_pct": 1
}]
; 元进度实现批(2026-09-21,设计权威 design/gdd/systems/冒险循环/元进度.md 定稿 v1.2):
@@ -90,3 +90,11 @@ conversion = [{
; 效果档 血+3/攻+1/甲+1 不变)——S1 一档(攻+2/血+6/甲+1 ≈ 120 GP)对齐「≈5 次 L1 通关开门」节奏
; (实测 L1 通关局收入 ≈29-35 GP)。换算率表维持全 0(墙/钥匙闭环不依赖该轴,留扩展)。
; 配套怪表改动见 enemy.tres 同日批注。provisional 未提交。
;
; 换算率标定批(2026-09-23——用户指令「现在标定」;读数报告 production/qa/conversion-calibration/
; calibration.raw.md,判定端 tools/balance_verifier.py ④ 段可复判):conversion 三行全 0 → 6/8/1 投值。
; 方法 = OFAT 直入口注入档(每属性 +1/+2 级 × 双画像 × L1/L2 × n=100,与白板同种子流配对)
; 实测每级带平移 生命 +6.5±0.7 / 攻击 +8.4±0.9 / 护甲 +1.4±0.4 点(逆方差加权合并)→ 取整投 6/8/1。
; 护甲行强条目重测义务随批履行:重测暴露并修复护甲断链(begin_battle hero_armor_class 参数此前
; 零消费——_defender_ac 主角分支本次接线,缺参/-1 = 表值兜底);修复后甲轴读数 +1.4±0.4 为
; 解锁后参数实测,非基线外推。战役轨迹三花费序(生命/攻击/护甲优先)随批复采验证。
@@ -15,6 +15,9 @@
# 大奖励立即确认 continue_to_next_level、adventure_finished 置 result/summary;外层每关重取
# runner 实例经 _drive_level 复用单关驱动)。run_level 驱动段抽成 _drive_level——签名与
# 返回结构零变化,行为逐位不变(MC 同种子对照闸承载)。
# 2026-09-23 换算率标定批:run_level 增 hero_override 尾参(永久强化 flat 注入透传至
# LevelRunner.start_level——缺省 {} 与既有调用方逐位一致零行为变更,契约同 run_adventure 同名尾参;
# 采集端 verifier_batch.gd inject 档消费,服务换算率标定 OFAT 读数)。
# 运行前提:db 已 load_tables;每个 run_level 全新 RngService(同种子 ⇒ 全程逐步可回放)。
# 实现注:信号处理体全部为静态方法 + bind 传状态——GDScript 多行 lambda 内嵌 match/控制流的解析不可靠。
extends RefCounted
@@ -22,13 +25,13 @@ 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") -> Dictionary:
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", 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, "level_index": level_index, "atk_pct": atk_pct, "hp_pct": hp_pct, "policy": policy, "route": route})
logger.log_run({"seed": seed_val, "level_index": level_index, "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 runner = load("res://core/level_runner.gd").new(db, rng)
@@ -44,7 +47,7 @@ static func run_level(db, seed_val: int, level_index: int, atk_pct: int, hp_pct:
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):
if not runner.start_level(level_index, seed_val, hero_override):
st.result = "start_failed"
runner.restore_hero_base()
if own_session:
@@ -8,10 +8,16 @@
# campaign 段 营地循环战役 × 双画像 × k 局——SaveManager 隔离档 + MetaProgression 真实账本:
# 逐局 hero_override() 注入 → 冒险 → settle_adventure 入账 → 花费策略买锻造,
# 回答「累计几次通关冒险后下一关可过」(元进度换算率标定的模拟读数源)
# 2026-09-23 换算率标定批:① level 段增 inject 注入档(永久强化 flat OFAT——标定换算率表的
# 边际读数:每档与基线同种子流配对,通关率差 = 该档的带平移量实测)+ levels 层裁剪;
# ② campaign 段增 armor_first 花费序(甲轴真实账本轨迹——护甲行强条目重测读数源)。
# 运行:Godot_console.exe --headless --path . --script res://smoke/verifier_batch.gd -- [k=v ...]
# n=60(直入口每格局数) na=40(基线冒险局数) k=40(战役局数) seed0=8000
# only=level,adventure,campaign(缺省全段) spend=hp_first|atk_first(锻造花费优先序)
# only=level,adventure,campaign(缺省全段) spend=hp_first|atk_first|armor_first(锻造花费优先序)
# inject=max_hp:3,atk:1(直入口 flat 注入档——三键 {max_hp, atk, armor_class},缺省全零白板)
# levels=1,2,3(直入口层集——标定批只收 1,2,第三层带未定零判定面)
# 种子纪律:unique 逐局独立(掷点测量修复批 A 案——seed0 + 全局游标,每掷点恰消费一次)。
# 同 seed0 跨档复用 = 配对读数(同序种子流——分析侧按局序配对消方差)。
# 日志合规:全段共享 playtest_logger 会话(与真人试玩同日志),分析工具可直接消费。
# 存档红线:战役档用 user://verifier_campaign_<画像>.json 隔离路径,段首清陈旧段尾清理——
# 绝不碰 user://save.json(与 smoke 存档隔离纪律同源)。
@@ -20,7 +26,9 @@ extends SceneTree
const SPEND_ORDERS := {
"hp_first": ["max_hp", "atk", "armor_class"],
"atk_first": ["atk", "max_hp", "armor_class"],
"armor_first": ["armor_class", "max_hp", "atk"],
}
const OVERRIDE_KEYS := ["atk", "max_hp", "armor_class"] # 注入契约三键(同 AdventureRunner 白板形)
var _cursor := 0 # unique 种子全局游标(三段连续消费,段间不重叠)
@@ -52,16 +60,15 @@ func _initialize() -> void:
func _parse_args(args: PackedStringArray) -> Dictionary:
var opt := {"n": 60, "na": 40, "k": 40, "seed0": 8000, "only": "level,adventure,campaign", "spend": "hp_first"}
var opt := {"n": 60, "na": 40, "k": 40, "seed0": 8000, "only": "level,adventure,campaign",
"spend": "hp_first", "inject": "", "levels": "1,2,3"}
for a in args:
var kv := String(a).split("=", true, 1)
if kv.size() != 2:
continue
var key := String(kv[0])
if opt.has(key):
if key == "only":
opt[key] = String(kv[1])
elif key == "spend":
if key in ["only", "spend", "inject", "levels"]:
opt[key] = String(kv[1])
else:
opt[key] = int(kv[1])
@@ -72,9 +79,29 @@ func _parse_args(args: PackedStringArray) -> Dictionary:
opt["only"] = segs
if not SPEND_ORDERS.has(String(opt.spend)):
opt["spend"] = "hp_first"
var lv: Array = []
for s in String(opt.levels).split(","):
var li := int(s)
if li >= 1 and li <= 3 and not lv.has(li):
lv.append(li)
lv.sort()
if lv.is_empty():
lv = [1, 2, 3]
opt["levels"] = lv
opt["override"] = _parse_inject(String(opt.inject))
return opt
# inject 档解析:"max_hp:3,atk:1" → 三键 flat 字典(缺键 = 0、负值 clamp 0、未知键丢弃)
func _parse_inject(raw: String) -> Dictionary:
var out := {"atk": 0, "max_hp": 0, "armor_class": 0}
for part in raw.split(","):
var kv := String(part).split(":", true, 1)
if kv.size() == 2 and String(kv[0]) in OVERRIDE_KEYS:
out[String(kv[0])] = maxi(0, int(kv[1]))
return out
func _next_seed(seed0: int) -> int:
var s: int = seed0 + _cursor
_cursor += 1
@@ -85,22 +112,35 @@ func _emit(row: Dictionary) -> void:
print("VJSON " + JSON.stringify(row))
# ---- level 段:直入口单关基线(零加成——分带批验收带判定的同口径读数) ----
# ---- level 段:直入口单关基线(零加成——分带批验收带判定的同口径读数);
# inject 档 = 同种子流配对的 OFAT 边际读数(换算率标定批——行带 override flat 三键) ----
func _seg_level(db, autoplay, logger, opt: Dictionary, counts: Dictionary) -> void:
var ov: Dictionary = opt.override
var tag := _inject_tag(ov)
for policy in ["normal", "perfect"]:
for li in [1, 2, 3]:
for li in opt.levels:
for i in range(int(opt.n)):
var seed_val := _next_seed(int(opt.seed0))
var st: Dictionary = autoplay.run_level(db, seed_val, li, 0, 0, false, logger, policy, "min")
var st: Dictionary = autoplay.run_level(db, seed_val, li, 0, 0, false, logger, policy, "min", ov)
_emit({
"seg": "level", "level": li, "policy": policy, "seed": seed_val,
"inject_tag": tag, "override": ov.duplicate(),
"result": String(st.result),
"defeat_node_id": int(st.defeat_node_id), "defeat_node_type": String(st.defeat_node_type),
"battles": int(st.battles), "rounds": int(st.rounds),
})
counts.level += 1
print("进度 level 第%d层 %s %d/%d" % [li, policy, int(opt.n), int(opt.n)])
print("进度 level 第%d层 %s %d/%d(%s)" % [li, policy, int(opt.n), int(opt.n), tag])
# 注入档人读标签(白板 = "白板";行键 inject_tag 供判定端分档聚合)
func _inject_tag(ov: Dictionary) -> String:
var parts: Array = []
for k in OVERRIDE_KEYS:
if int(ov.get(k, 0)) != 0:
parts.append("%s+%d" % [k, int(ov[k])])
return "白板" if parts.is_empty() else ",".join(parts)
# ---- adventure 段:全冒险连探基线(零永久强化——止步分布与败退节点归因) ----
@@ -148,6 +188,7 @@ func _seg_campaign(db, autoplay, logger, opt: Dictionary, counts: Dictionary) ->
var row := _adventure_row(st, policy, seed_val)
row["seg"] = "campaign"
row["run_no"] = r + 1
row["spend"] = String(opt.spend)
row["override"] = ov
row["keystone_before"] = keystone_before
if st.summary.is_empty():