merge: 换算率标定批——conversion 三行投值 6/9/1(冒险条件口径)+ 护甲断链接线修复 + 判定端表值驱动(用户指令 2026-09-23)

This commit is contained in:
DKChan committed 2026-09-23 08:46:07 +08:00
commit 277b0173ba
13 files changed
+2620 -110

No files matched your search

+1 -9
View File
@@ -255,14 +255,6 @@
**状态**:待办
#### 区间平移换算率数值未定
**现状**:"生命上限 +10 ≈ 多撑约 1 轮、攻击翻倍 ≈ 击杀区间约减半"是量级估算(provisional);作为元进度接入口,数值待实测标定。
**归属**:随[元进度](systems/冒险循环/元进度.md)定稿
**状态**:待办(2026-09-21 元进度实现批收窄)——**表结构已建**(`data/tables/meta.tres` conversion 行族三行,数值占位 0 = 未标定合法态);剩余面 = 折算数值随设计批模拟读数标定 + 护甲行强条目须按解锁后参数重测
#### 三方向各自的解锁清单与定价
@@ -270,7 +262,7 @@
**归属**:元进度设计批
**状态**:部分收口(2026-09-21 元进度实现批再收窄)——**三柜台结构与 provisional 定价已落地**(属性锻造三档血+3/攻+1/甲+1、base 10 公比 150;Boss 两档 L1 5/1·L2 8/2·L3 12/3;工坊六件阈值 1/1/2/2/3/3 按总通关次数——全部读 `data/tables/meta.tres`);剩余面 = 定价数字与 Boss 两档、换算率数值随设计批配读数标定
**状态**:部分收口(2026-09-21 元进度实现批再收窄;2026-09-23 换算率标定批再收窄)——**三柜台结构与 provisional 定价已落地**(属性锻造三档血+3/攻+1/甲+1、base 10 公比 150;Boss 两档 L1 5/1·L2 8/2·L3 12/3;工坊六件阈值 1/1/2/2/3/3 按总通关次数——全部读 `data/tables/meta.tres`);换算率三行已投值 6/9/1(同批,冒险条件口径实测,护甲行强条目重测履行);剩余面 = 定价数字与 Boss 两档
#### 解锁界面与钥石呈现的交互形态
+9 -6
View File
@@ -74,13 +74,15 @@
解锁让玩家变强,难度验收区间随之需要平移——双方约定(难度曲线与Boss 定稿口径)永久强化的影响**不直接改难度判据**,而是按统一换算率折算成通关率带的平移量后套用:完美档 gate 与 normal 目标带的判读口径不变,带随解锁等级平移。
换算率表(registry 键 `meta_progression_conversion_table`,数值行读 `data/tables/`)首版登记三个属性维度(2026-09-18 拍板;"行" = 表内一条记录,一个属性维度即一行条目,全仓与"行值"同义):
换算率表(registry 键 `meta_progression_conversion_table`,数值行读 `data/tables/meta.tres` conversion 行族)登记三个属性维度(2026-09-18 拍板;"行" = 表内一条记录,一个属性维度即一行条目,全仓与"行值"同义)。现行表值(2026-09-23 换算率标定批投值——OFAT 冒险注入档与白板同种子配对的逐关条件通关率差实测,冒险条件口径 + 当日数值包现行态;报告 `production/qa/conversion-calibration/calibration.raw.md`,判定端 ④ 段可复判):
| 换算行 | 折算语义 | 纪律 |
| 换算行 | 折算语义 | 每级带平移量(现行表值) |
|---|---|---|
| 生命上限强化 | 每级折算为通关率带平移量 | 数值随设计批模拟读数标定 |
| 基础攻击强化 | 每级折算为通关率带平移量 | 同上 |
| 护甲等级强化 | 每级折算为通关率带平移量 | **强条目**:护甲等级牵动命中判定全链,验收读数必须按解锁后参数重测,不得沿用基线读数外推 |
| 生命上限强化 | 每级折算为通关率带平移量 | **6 点** |
| 基础攻击强化 | 每级折算为通关率带平移量 | **9 点** |
| 护甲等级强化 | 每级折算为通关率带平移量 | **1 点** |
判定端(tools/balance_verifier.py)表值驱动消费:验收带按解锁状态 flat÷每级flat×表值平移后套用,表改则判定随动。护甲行强条目重测义务已于标定批履行(解锁后参数实测,非基线外推);标定同时暴露并修复护甲断链(begin_battle `hero_armor_class` 参数零消费——主角护甲锻造与加算区护甲轴的生效值此前未进战斗),修复见 battle_orchestrator `_defender_ac` 批注。
后续柜台商品(道具工坊钥石价、奖励格图鉴)上架时逐条补行——每次上架新商品伴随一行换算登记,逐行归因纪律不破。
@@ -121,7 +123,7 @@ MVP 不要求本系统的闭环([不做清单](../../05-不做清单.md)整体
| P1 | 三柜台内容铺量与定价数字 | 柜台结构已定稿;清单明细、几何定价的起步价与公比、Boss 两档产出数值,随设计批配读数投值 |
| P1 | 道具工坊钥石直购价 | 首版只挂通关次数渠道(零定价依赖);定价体系建成后补价,收敛双渠道 |
| P1 | 奖励格图鉴的格子内容 | 排 Alpha,随[地图战斗资源](../战斗循环/地图战斗资源.md)差异化批 |
| P1 | 换算率表数值行标定 | 首版三行的折算数值随模拟读数定;护甲行验收重测义务随批 |
| P1 | 换算率表数值行标定 | **已完成**(2026-09-23 换算率标定批——首版三行投值 6/8/1,护甲行强条目重测随批履行并修复护甲断链) |
| P2 | 营地界面与解锁呈现实现 | 归[HUD 框架](../表现与支撑/HUD框架.md)协作批,以表现层样张过审为前置 |
## 调参表
@@ -158,3 +160,4 @@ MVP 不要求本系统的闭环([不做清单](../../05-不做清单.md)整体
| v1.1 | 2026-09-18 | **数值语言拍板批(与 01-核心循环 数值语言条款 / 难度曲线与Boss / registry / 06-待办清单 / 切片代码同批)**:营地三柜台节新增投值语言锁定条款——属性投值语言锁定 flat 整数加减,数值投值仍归设计批 |
| v1.2 | 2026-09-20 | GP+商店房批注记(知会性质——与冒险结构 v0.5 同批):钥石节新增与 GP 的分工一行——钥石 = 跨局持久解锁、GP(「金币」)= 冒险内一次性属性提升、局内清零不入存档,双币生命周期切分成文、永不互兑。机制零改动 |
| v1.3 | 2026-09-21 | **元进度实现批(与切片代码 / registry / 02 / 03 / 05 / 06 同批)**:本批 = 本文的实现批——MetaTable 四行族建表(`data/tables/meta.tres`,数值全 provisional 归设计批配读数);MetaProgression 账本服务(余额/几何定价/购买/工坊解锁/Boss 两档入账/注入计算,Core 层零 UI 零信号);注入通道落地(start_adventure params.hero_override → LevelRunner base_override,叠加序 = 表值+永久强化 flat+加算区 flat × 测试面板 pct);呈现面(营地覆盖层三柜台+主菜单入口+A 区顶栏钥石余额+结算屏入账明细,ui_kit 程序占位——视觉终稿归 HUD 协作批)。正文零改动(实现面不回流正文——正文零历史叙述纪律) |
| v1.4 | 2026-09-23 | **换算率标定批(用户指令「现在标定」;与切片代码 / registry / 06-待办清单 / 判定端工具同批)**:换算率节回填现行表值(生命 6 / 攻击 9 / 护甲 1 点每级——OFAT 冒险注入档与白板同种子配对的逐关条件通关率差实测,冒险条件口径;同日单关直入口口径退役使首轮直入口面读数作废重测);护甲行强条目重测履行并暴露修复护甲断链(begin_battle hero_armor_class 参数零消费→_defender_ac 接线);判定端带平移表值驱动接线(带重锚后随换算率平移判);排期表 P1 行销项 |
+17 -1
View File
@@ -604,7 +604,7 @@ entities:
type: schema_data
owner: design/gdd/systems/冒险循环/元进度.md
attributes:
table: 永久强化换算率表(数据配置 承载;键结构 元进度定稿批 2026-09-18 登记,数值行读 `data/tables/` 随元进度设计批投值——本条只登记结构,零数值。**已建表**:meta.tres conversion 行族三行落地,数值占位 0 = 未标定合法态,provisional 待设计批标定(护甲行强条目在案)——元进度实现批 2026-09-21 注记)
table: 永久强化换算率表(数据配置 承载;键结构 元进度定稿批 2026-09-18 登记,数值行读 `data/tables/meta.tres` conversion 行族。**已标定投值**:三行 生命 6 / 攻击 9 / 护甲 1 点每级——换算率标定批 2026-09-23,OFAT 冒险注入档与白板同种子配对的逐关条件通关率差实测(每级带平移 +6.0±0.7 / +8.8±0.8 / +1.2±0.4 点取整;冒险条件口径 + 当日 L2 收紧/大奖励 10→5 后现行数值包——直入口口径同日退役使首轮直入口面读数作废),护甲行强条目重测随批履行并暴露修复护甲断链(begin_battle hero_armor_class 参数零消费→_defender_ac 接线);判定端 balance_verifier.py 表值驱动消费(带重锚后随平移判);读数报告 production/qa/conversion-calibration/calibration.raw.md)
kind: 区间平移换算率(2026-09-18 用户拍板启用)——永久强化对难度的影响不直接改难度判据,按统一换算率折算为通关率带平移量后套用(完美档 gate 与 normal 目标带判读口径不变,带随解锁等级平移)
key: (conversion_row_id)——一行一柜台商品类型
first_version_rows: 首版登记三个属性维度(表内即三行条目;用户拍板 2026-09-18,"维度"为拍板用语、"行"为表内同义术语):生命上限强化 / 基础攻击强化 / 护甲等级强化——后续商品上架(道具工坊钥石价、奖励格图鉴)逐条补登记,每次上架伴随一行换算登记(逐行归因纪律)
@@ -861,3 +861,19 @@ entities:
# 混淆使任何窄种子窗表现如随机抽样(改版后同窗 χ²=13.9 达标);黄金向量 8 常量全量重登记(Python 参考
# 实现逐位交叉验证)、R2 旧向量作废;逐 scope 复放语义/计数器契约/存档快照契约不变;冒烟 316 项 0 失败
# (含 1 条条件断言因新序列不再触发,317→316 良性);同种子历史坐标随批作废(重测义务=验收批阶段一重跑)。
# 2026-09-23 换算率标定批(用户指令「现在标定」;与 meta.tres conversion 投值 / 元进度 v1.4 /
# 06-待办清单删条 / 判定端 balance_verifier.py ④ 段 / 切片代码同批):
# ① meta_progression_conversion_table 投值收口:三行 生命 6 / 攻击 9 / 护甲 1 点每级
# (OFAT 冒险注入档×双画像×na=100 与白板同种子流配对,逐关条件通关率差实测每级带平移
# +6.0±0.7 / +8.8±0.8 / +1.2±0.4 点配对 bootstrap 合并取整;读数报告
# production/qa/conversion-calibration/calibration.raw.md。测量面两换:首轮直入口面读数
# 6/8/1 因同日单关直入口口径退役作废,且冒险条件口径 + L2 收紧/大奖励 10→5 现行数值包重测);
# ② 护甲行强条目重测履行=重测暴露护甲断链:begin_battle hero_armor_class 参数自元进度实现批
# 传入以来零消费(_defender_ac 主角分支只读 db.hero 表值)——永久强化护甲档与冒险加算区
# 护甲轴的生效值未进战斗;本批接线(缺参/-1 = 表值兜底,白板路径逐位不变,冒烟 553 项 0 失败
# 含 MC 同种子对照闸);修复后甲轴读数 +1.2±0.4 为解锁后参数实测;
# ③ 判定端带平移接线:balance_verifier.py 启动解析 meta.tres(表值锚配置推导化同纪律),
# 注入/锻造状态按 flat÷每级flat×表值平移验收带后判读,campaign 检查点同口径——
# 带重锚(曲线调参批)后平移判自动启用,带待重锚期只报平移量不出判定;
# ④ 06-待办清单「区间平移换算率数值未定」条目解决即删;同日 数值设计批 2026-09-22「维持全 0」
# 批注由用户「现在标定」指令覆盖。
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,296 @@
# 数值验证报告(20260923-084327)
- 采集规模:基线冒险 100 局/画像 · 战役 60 局/画像 · 种子 8000 起逐局独立 · 锻造花费策略 = hp_first
- 切片版本:7fd6fbd · 口径:逐关读数 = 冒险条件口径(单关直入口口径已退役 2026-09-23,带待重锚只测不判——重锚归曲线调参批) · 换算率表值(meta.tres 启动解析):生命上限每级平移6点 · 基础攻击每级平移9点 · 护甲等级每级平移1点
## ① 逐关条件通关率(冒险口径——打通 k-1 关才到达第 k 关;带待重锚只测不判)
| 层 | 画像 | 通关/到达 | 条件通关率±95%CI | 验收带 |
|--------|--------|-----------|------------------|--------|
| 第一层 | 普通档 | 53/100 | 53.0% ±9.8 | 未定 |
| 第二层 | 普通档 | 1/53 | 1.9% ±3.7 | 未定 |
| 第三层 | 普通档 | 0/1 | 0.0% ±0.0 | 未定 |
| 第一层 | 完美档 | 86/100 | 86.0% ±6.8 | 未定 |
| 第二层 | 完美档 | 15/86 | 17.4% ±8.0 | 未定 |
| 第三层 | 完美档 | 0/15 | 0.0% ±0.0 | 未定 |
## ② 零永久强化冒险:止步分布与败退节点(用户判据③)
### 普通档(100 局)
- 止步分布:止步第1层 47(47%)、止步第2层 52(52%)、止步第3层 1(1%)
- 败退节点分布(失败局归因):
- 第二层·节点1·战斗 ×23
- 第一层·节点4·战斗 ×20
- 第一层·节点6·Boss ×20
- 第二层·节点9·Boss ×19
- 第二层·节点6·战斗 ×8
- 第一层·节点3·战斗 ×7
- 第二层·节点0·战斗 ×2
- 第三层·节点1·战斗 ×1
### 完美档(100 局)
- 止步分布:止步第1层 14(14%)、止步第2层 71(71%)、止步第3层 15(15%)
- 败退节点分布(失败局归因):
- 第二层·节点1·战斗 ×28
- 第二层·节点9·Boss ×27
- 第二层·节点6·战斗 ×16
- 第一层·节点4·战斗 ×11
- 第三层·节点1·战斗 ×7
- 第三层·节点6·战斗 ×5
- 第三层·节点9·Boss ×2
- 第一层·节点6·Boss ×2
- 第一层·节点3·战斗 ×1
- 第三层·节点0·战斗 ×1
## ③ 营地循环爬升(用户判据④:5 次通关 → 下一层可过)
### 普通档 · 生命优先(60 局战役)
- 钥石:全程入账 1262 · 终态余额 26 · 锻造终态 = 生命+18/攻击+6/护甲+6
- 道具工坊解锁时点(魔法道具面,局内效果未实装只报不判):显影尘 第 3 局、工兵令 第 3 局、探宝罗盘 第 5 局、探巢摆钟 第 5 局、地穴全图卷 第 6 局、爆破工兵包 第 6 局
- 判据:累计通过第一层 5 次后,第二层条件通关率(滑窗 10 局)——带未定(只测不判)(触发时注入 = 生命+3/攻击+1/护甲+0,表值平移 +15.0 点——带重锚后随平移判)
- 判据:累计通过第二层 5 次后,第三层条件通关率(滑窗 10 局)——带未定(只测不判)(触发时注入 = 生命+9/攻击+3/护甲+3,表值平移 +48.0 点——带重锚后随平移判)
| 局 | 注入(flat) | 钥石 | 第1层率(窗) | 第2层率(窗) | 第3层率(窗) | 终局 |
|--------|--------------|------|-------------|-------------|-------------|-----------|
| 第1局 | 血0/攻0/甲0 | 0 | 0%(n=1) | — | — | 止步第1层 |
| 第4局 | 血0/攻0/甲0 | 15 | 25%(n=4) | — | — | 止步第1层 |
| 第7局 | 血3/攻0/甲0 | 17 | 43%(n=7) | — | — | 止步第1层 |
| 第10局 | 血3/攻0/甲0 | 8 | 40%(n=10) | 0%(n=4) | — | 止步第2层 |
| 第13局 | 血3/攻1/甲0 | 19 | 40%(n=10) | — | — | 止步第1层 |
| 第16局 | 血6/攻1/甲0 | 11 | 40%(n=10) | 0%(n=7) | — | 止步第2层 |
| 第19局 | 血6/攻1/甲1 | 12 | 70%(n=10) | 10%(n=10) | 0%(n=1) | 止步第3层 |
| 第22局 | 血6/攻2/甲2 | 15 | 80%(n=10) | 10%(n=10) | — | 止步第2层 |
| 第25局 | 血9/攻2/甲2 | 4 | 100%(n=10) | 30%(n=10) | 33%(n=3) | 止步第3层 |
| 第28局 | 血9/攻3/甲3 | 29 | 100%(n=10) | 50%(n=10) | 40%(n=5) | 止步第3层 |
| 第31局 | 血9/攻3/甲3 | 32 | 100%(n=10) | 60%(n=10) | 43%(n=7) | 通关 |
| 第34局 | 血12/攻3/甲3 | 10 | 100%(n=10) | 60%(n=10) | 38%(n=8) | 止步第3层 |
| 第37局 | 血12/攻4/甲4 | 38 | 100%(n=10) | 70%(n=10) | 50%(n=10) | 通关 |
| 第40局 | 血15/攻4/甲4 | 33 | 100%(n=10) | 80%(n=10) | 50%(n=10) | 止步第3层 |
| 第43局 | 血15/攻4/甲4 | 28 | 100%(n=10) | 100%(n=10) | 60%(n=10) | 通关 |
| 第46局 | 血15/攻5/甲5 | 23 | 100%(n=10) | 100%(n=10) | 70%(n=10) | 止步第3层 |
| 第49局 | 血15/攻5/甲5 | 131 | 100%(n=10) | 100%(n=10) | 70%(n=10) | 通关 |
| 第52局 | 血18/攻5/甲5 | 76 | 100%(n=10) | 100%(n=10) | 80%(n=10) | 止步第3层 |
| 第55局 | 血18/攻5/甲5 | 21 | 100%(n=10) | 100%(n=10) | 70%(n=10) | 止步第3层 |
| 第58局 | 血18/攻6/甲5 | 129 | 100%(n=10) | 100%(n=10) | 80%(n=10) | 通关 |
### 完美档 · 生命优先(60 局战役)
- 钥石:全程入账 1810 · 终态余额 124 · 锻造终态 = 生命+21/攻击+7/护甲+6
- 道具工坊解锁时点(魔法道具面,局内效果未实装只报不判):显影尘 第 1 局、工兵令 第 1 局、探宝罗盘 第 2 局、探巢摆钟 第 2 局、地穴全图卷 第 3 局、爆破工兵包 第 3 局
- 判据:累计通过第一层 5 次后,第二层条件通关率(滑窗 10 局)——带未定(只测不判)(触发时注入 = 生命+6/攻击+1/护甲+0,表值平移 +21.0 点——带重锚后随平移判)
- 判据:累计通过第二层 5 次后,第三层条件通关率(滑窗 10 局)——带未定(只测不判)(触发时注入 = 生命+6/攻击+2/护甲+2,表值平移 +32.0 点——带重锚后随平移判)
| 局 | 注入(flat) | 钥石 | 第1层率(窗) | 第2层率(窗) | 第3层率(窗) | 终局 |
|--------|--------------|------|-------------|-------------|-------------|-----------|
| 第1局 | 血0/攻0/甲0 | 15 | 100%(n=1) | 0%(n=1) | — | 止步第2层 |
| 第4局 | 血6/攻0/甲0 | 16 | 100%(n=4) | 25%(n=4) | — | 止步第2层 |
| 第7局 | 血6/攻2/甲0 | 3 | 100%(n=7) | 43%(n=7) | 0%(n=3) | 止步第3层 |
| 第10局 | 血6/攻2/甲2 | 30 | 100%(n=10) | 50%(n=10) | 0%(n=5) | 止步第3层 |
| 第13局 | 血9/攻2/甲2 | 19 | 100%(n=10) | 60%(n=10) | 14%(n=7) | 通关 |
| 第16局 | 血9/攻3/甲2 | 19 | 100%(n=10) | 60%(n=10) | 12%(n=8) | 止步第3层 |
| 第19局 | 血12/攻3/甲3 | 60 | 100%(n=10) | 70%(n=10) | 40%(n=10) | 通关 |
| 第22局 | 血12/攻4/甲3 | 8 | 100%(n=10) | 80%(n=10) | 50%(n=10) | 止步第3层 |
| 第25局 | 血12/攻4/甲4 | 90 | 100%(n=10) | 100%(n=10) | 50%(n=10) | 止步第3层 |
| 第28局 | 血15/攻4/甲4 | 98 | 100%(n=10) | 100%(n=10) | 60%(n=10) | 通关 |
| 第31局 | 血15/攻5/甲4 | 93 | 100%(n=10) | 100%(n=10) | 60%(n=10) | 通关 |
| 第34局 | 血15/攻5/甲5 | 101 | 100%(n=10) | 100%(n=10) | 80%(n=10) | 通关 |
| 第37局 | 血18/攻5/甲5 | 59 | 100%(n=10) | 100%(n=10) | 90%(n=10) | 通关 |
| 第40局 | 血18/攻5/甲5 | 17 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
| 第43局 | 血18/攻6/甲5 | 112 | 100%(n=10) | 100%(n=10) | 90%(n=10) | 止步第3层 |
| 第46局 | 血18/攻6/甲6 | 70 | 100%(n=10) | 100%(n=10) | 90%(n=10) | 通关 |
| 第49局 | 血18/攻6/甲6 | 178 | 100%(n=10) | 100%(n=10) | 90%(n=10) | 通关 |
| 第52局 | 血21/攻6/甲6 | 61 | 100%(n=10) | 100%(n=10) | 90%(n=10) | 通关 |
| 第55局 | 血21/攻6/甲6 | 169 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
| 第58局 | 血21/攻7/甲6 | 52 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
### 普通档 · 攻击优先(60 局战役)
- 钥石:全程入账 1311 · 终态余额 75 · 锻造终态 = 生命+18/攻击+6/护甲+6
- 道具工坊解锁时点(魔法道具面,局内效果未实装只报不判):显影尘 第 3 局、工兵令 第 3 局、探宝罗盘 第 5 局、探巢摆钟 第 5 局、地穴全图卷 第 6 局、爆破工兵包 第 6 局
- 判据:累计通过第一层 5 次后,第二层条件通关率(滑窗 10 局)——带未定(只测不判)(触发时注入 = 生命+3/攻击+2/护甲+0,表值平移 +24.0 点——带重锚后随平移判)
- 判据:累计通过第二层 5 次后,第三层条件通关率(滑窗 10 局)——带未定(只测不判)(触发时注入 = 生命+9/攻击+3/护甲+3,表值平移 +48.0 点——带重锚后随平移判)
| 局 | 注入(flat) | 钥石 | 第1层率(窗) | 第2层率(窗) | 第3层率(窗) | 终局 |
|--------|--------------|------|-------------|-------------|-------------|-----------|
| 第1局 | 血0/攻0/甲0 | 0 | 0%(n=1) | — | — | 止步第1层 |
| 第4局 | 血0/攻0/甲0 | 15 | 25%(n=4) | — | — | 止步第1层 |
| 第7局 | 血0/攻1/甲0 | 17 | 43%(n=7) | — | — | 止步第1层 |
| 第10局 | 血0/攻1/甲0 | 16 | 40%(n=10) | 25%(n=4) | 0%(n=1) | 止步第3层 |
| 第13局 | 血0/攻2/甲0 | 7 | 40%(n=10) | 20%(n=5) | — | 止步第2层 |
| 第16局 | 血3/攻2/甲0 | 9 | 40%(n=10) | 14%(n=7) | — | 止步第2层 |
| 第19局 | 血6/攻2/甲1 | 24 | 70%(n=10) | 20%(n=10) | 0%(n=2) | 止步第3层 |
| 第22局 | 血6/攻2/甲2 | 27 | 90%(n=10) | 20%(n=10) | — | 止步第2层 |
| 第25局 | 血6/攻3/甲2 | 16 | 100%(n=10) | 30%(n=10) | 25%(n=4) | 止步第3层 |
| 第28局 | 血9/攻3/甲3 | 41 | 100%(n=10) | 50%(n=10) | 33%(n=6) | 止步第3层 |
| 第31局 | 血9/攻4/甲3 | 44 | 100%(n=10) | 60%(n=10) | 38%(n=8) | 通关 |
| 第34局 | 血12/攻4/甲3 | 46 | 100%(n=10) | 80%(n=10) | 30%(n=10) | 止步第3层 |
| 第37局 | 血12/攻4/甲4 | 74 | 100%(n=10) | 90%(n=10) | 40%(n=10) | 通关 |
| 第40局 | 血12/攻5/甲4 | 82 | 100%(n=10) | 100%(n=10) | 60%(n=10) | 通关 |
| 第43局 | 血15/攻5/甲4 | 77 | 100%(n=10) | 100%(n=10) | 70%(n=10) | 通关 |
| 第46局 | 血15/攻5/甲5 | 72 | 100%(n=10) | 100%(n=10) | 80%(n=10) | 止步第3层 |
| 第49局 | 血15/攻5/甲5 | 30 | 100%(n=10) | 100%(n=10) | 80%(n=10) | 通关 |
| 第52局 | 血15/攻6/甲5 | 125 | 100%(n=10) | 100%(n=10) | 80%(n=10) | 止步第3层 |
| 第55局 | 血18/攻6/甲5 | 70 | 100%(n=10) | 100%(n=10) | 70%(n=10) | 止步第3层 |
| 第58局 | 血18/攻6/甲5 | 28 | 100%(n=10) | 100%(n=10) | 80%(n=10) | 通关 |
### 完美档 · 攻击优先(60 局战役)
- 钥石:全程入账 1798 · 终态余额 112 · 锻造终态 = 生命+21/攻击+7/护甲+6
- 道具工坊解锁时点(魔法道具面,局内效果未实装只报不判):显影尘 第 1 局、工兵令 第 1 局、探宝罗盘 第 2 局、探巢摆钟 第 2 局、地穴全图卷 第 3 局、爆破工兵包 第 3 局
- 判据:累计通过第一层 5 次后,第二层条件通关率(滑窗 10 局)——带未定(只测不判)(触发时注入 = 生命+3/攻击+2/护甲+0,表值平移 +24.0 点——带重锚后随平移判)
- 判据:累计通过第二层 5 次后,第三层条件通关率(滑窗 10 局)——带未定(只测不判)(触发时注入 = 生命+6/攻击+3/护甲+2,表值平移 +41.0 点——带重锚后随平移判)
| 局 | 注入(flat) | 钥石 | 第1层率(窗) | 第2层率(窗) | 第3层率(窗) | 终局 |
|--------|--------------|------|-------------|-------------|-------------|-----------|
| 第1局 | 血0/攻0/甲0 | 15 | 100%(n=1) | 0%(n=1) | — | 止步第2层 |
| 第4局 | 血0/攻2/甲0 | 16 | 100%(n=4) | 25%(n=4) | — | 止步第2层 |
| 第7局 | 血3/攻2/甲0 | 11 | 100%(n=7) | 29%(n=7) | 0%(n=2) | 止步第3层 |
| 第10局 | 血6/攻2/甲1 | 3 | 100%(n=10) | 40%(n=10) | 0%(n=4) | 止步第3层 |
| 第13局 | 血6/攻3/甲2 | 7 | 100%(n=10) | 50%(n=10) | 17%(n=6) | 通关 |
| 第16局 | 血9/攻3/甲2 | 7 | 100%(n=10) | 60%(n=10) | 14%(n=7) | 止步第3层 |
| 第19局 | 血9/攻4/甲3 | 48 | 100%(n=10) | 70%(n=10) | 40%(n=10) | 通关 |
| 第22局 | 血12/攻4/甲3 | 63 | 100%(n=10) | 80%(n=10) | 50%(n=10) | 止步第3层 |
| 第25局 | 血12/攻4/甲4 | 78 | 100%(n=10) | 100%(n=10) | 50%(n=10) | 止步第3层 |
| 第28局 | 血12/攻5/甲4 | 86 | 100%(n=10) | 100%(n=10) | 60%(n=10) | 通关 |
| 第31局 | 血15/攻5/甲4 | 81 | 100%(n=10) | 100%(n=10) | 60%(n=10) | 通关 |
| 第34局 | 血15/攻5/甲5 | 89 | 100%(n=10) | 100%(n=10) | 80%(n=10) | 通关 |
| 第37局 | 血15/攻6/甲5 | 47 | 100%(n=10) | 100%(n=10) | 90%(n=10) | 通关 |
| 第40局 | 血15/攻6/甲5 | 5 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
| 第43局 | 血18/攻6/甲5 | 100 | 100%(n=10) | 100%(n=10) | 90%(n=10) | 止步第3层 |
| 第46局 | 血18/攻6/甲6 | 58 | 100%(n=10) | 100%(n=10) | 90%(n=10) | 通关 |
| 第49局 | 血18/攻6/甲6 | 166 | 100%(n=10) | 100%(n=10) | 90%(n=10) | 通关 |
| 第52局 | 血18/攻7/甲6 | 49 | 100%(n=10) | 100%(n=10) | 90%(n=10) | 通关 |
| 第55局 | 血18/攻7/甲6 | 157 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
| 第58局 | 血21/攻7/甲6 | 40 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
### 普通档 · 护甲优先(60 局战役)
- 钥石:全程入账 1145 · 终态余额 59 · 锻造终态 = 生命+18/攻击+5/护甲+6
- 道具工坊解锁时点(魔法道具面,局内效果未实装只报不判):显影尘 第 3 局、工兵令 第 3 局、探宝罗盘 第 5 局、探巢摆钟 第 5 局、地穴全图卷 第 6 局、爆破工兵包 第 6 局
- 判据:累计通过第一层 5 次后,第二层条件通关率(滑窗 10 局)——带未定(只测不判)(触发时注入 = 生命+3/攻击+0/护甲+1,表值平移 +7.0 点——带重锚后随平移判)
- 判据:累计通过第二层 5 次后,第三层条件通关率(滑窗 10 局)——带未定(只测不判)(触发时注入 = 生命+9/攻击+3/护甲+3,表值平移 +48.0 点——带重锚后随平移判)
| 局 | 注入(flat) | 钥石 | 第1层率(窗) | 第2层率(窗) | 第3层率(窗) | 终局 |
|--------|--------------|------|-------------|-------------|-------------|-----------|
| 第1局 | 血0/攻0/甲0 | 0 | 0%(n=1) | — | — | 止步第1层 |
| 第4局 | 血0/攻0/甲0 | 15 | 25%(n=4) | — | — | 止步第1层 |
| 第7局 | 血0/攻0/甲1 | 17 | 43%(n=7) | — | — | 止步第1层 |
| 第10局 | 血0/攻0/甲1 | 8 | 40%(n=10) | 0%(n=4) | — | 止步第2层 |
| 第13局 | 血3/攻0/甲1 | 8 | 30%(n=10) | — | — | 止步第1层 |
| 第16局 | 血3/攻0/甲1 | 19 | 20%(n=10) | — | — | 止步第1层 |
| 第19局 | 血3/攻0/甲2 | 11 | 40%(n=10) | — | — | 止步第1层 |
| 第22局 | 血3/攻1/甲2 | 13 | 50%(n=10) | 0%(n=9) | — | 止步第2层 |
| 第25局 | 血3/攻1/甲3 | 12 | 60%(n=10) | 20%(n=10) | 50%(n=2) | 止步第3层 |
| 第28局 | 血6/攻2/甲3 | 27 | 70%(n=10) | 30%(n=10) | 33%(n=3) | 止步第3层 |
| 第31局 | 血9/攻2/甲3 | 40 | 90%(n=10) | 40%(n=10) | 50%(n=4) | 通关 |
| 第34局 | 血9/攻3/甲3 | 40 | 100%(n=10) | 40%(n=10) | 40%(n=5) | 止步第3层 |
| 第37局 | 血9/攻3/甲4 | 55 | 100%(n=10) | 60%(n=10) | 38%(n=8) | 止步第3层 |
| 第40局 | 血12/攻3/甲4 | 16 | 100%(n=10) | 80%(n=10) | 40%(n=10) | 止步第3层 |
| 第43局 | 血12/攻4/甲4 | 99 | 100%(n=10) | 90%(n=10) | 60%(n=10) | 通关 |
| 第46局 | 血12/攻4/甲5 | 94 | 100%(n=10) | 90%(n=10) | 70%(n=10) | 止步第3层 |
| 第49局 | 血15/攻4/甲5 | 2 | 100%(n=10) | 90%(n=10) | 80%(n=10) | 通关 |
| 第52局 | 血15/攻5/甲5 | 97 | 100%(n=10) | 100%(n=10) | 80%(n=10) | 止步第3层 |
| 第55局 | 血15/攻5/甲6 | 42 | 100%(n=10) | 100%(n=10) | 70%(n=10) | 止步第3层 |
| 第58局 | 血15/攻5/甲6 | 0 | 100%(n=10) | 100%(n=10) | 80%(n=10) | 通关 |
### 完美档 · 护甲优先(60 局战役)
- 钥石:全程入账 1674 · 终态余额 213 · 锻造终态 = 生命+18/攻击+6/护甲+7
- 道具工坊解锁时点(魔法道具面,局内效果未实装只报不判):显影尘 第 1 局、工兵令 第 1 局、探宝罗盘 第 2 局、探巢摆钟 第 2 局、地穴全图卷 第 3 局、爆破工兵包 第 3 局
- 判据:累计通过第一层 5 次后,第二层条件通关率(滑窗 10 局)——带未定(只测不判)(触发时注入 = 生命+3/攻击+0/护甲+2,表值平移 +8.0 点——带重锚后随平移判)
- 判据:累计通过第二层 5 次后,第三层条件通关率(滑窗 10 局)——带未定(只测不判)(触发时注入 = 生命+9/攻击+3/护甲+3,表值平移 +48.0 点——带重锚后随平移判)
| 局 | 注入(flat) | 钥石 | 第1层率(窗) | 第2层率(窗) | 第3层率(窗) | 终局 |
|--------|--------------|------|-------------|-------------|-------------|-----------|
| 第1局 | 血0/攻0/甲0 | 15 | 100%(n=1) | 0%(n=1) | — | 止步第2层 |
| 第4局 | 血0/攻0/甲1 | 8 | 100%(n=4) | 0%(n=4) | — | 止步第2层 |
| 第7局 | 血3/攻0/甲2 | 9 | 100%(n=7) | 14%(n=7) | — | 止步第2层 |
| 第10局 | 血6/攻1/甲2 | 12 | 100%(n=10) | 10%(n=10) | — | 止步第2层 |
| 第13局 | 血6/攻2/甲2 | 16 | 100%(n=10) | 30%(n=10) | 33%(n=3) | 通关 |
| 第16局 | 血6/攻2/甲3 | 4 | 100%(n=10) | 20%(n=10) | — | 止步第2层 |
| 第19局 | 血9/攻3/甲3 | 41 | 100%(n=10) | 50%(n=10) | 33%(n=6) | 通关 |
| 第22局 | 血9/攻3/甲4 | 44 | 100%(n=10) | 60%(n=10) | 38%(n=8) | 止步第3层 |
| 第25局 | 血12/攻3/甲4 | 46 | 100%(n=10) | 80%(n=10) | 30%(n=10) | 止步第3层 |
| 第28局 | 血12/攻4/甲4 | 87 | 100%(n=10) | 90%(n=10) | 50%(n=10) | 通关 |
| 第31局 | 血12/攻4/甲5 | 82 | 100%(n=10) | 100%(n=10) | 50%(n=10) | 通关 |
| 第34局 | 血15/攻4/甲5 | 65 | 100%(n=10) | 90%(n=10) | 70%(n=10) | 通关 |
| 第37局 | 血15/攻5/甲5 | 73 | 100%(n=10) | 90%(n=10) | 90%(n=10) | 通关 |
| 第40局 | 血15/攻5/甲5 | 31 | 100%(n=10) | 90%(n=10) | 90%(n=10) | 通关 |
| 第43局 | 血15/攻5/甲6 | 126 | 100%(n=10) | 100%(n=10) | 90%(n=10) | 止步第3层 |
| 第46局 | 血18/攻5/甲6 | 84 | 100%(n=10) | 100%(n=10) | 90%(n=10) | 通关 |
| 第49局 | 血18/攻6/甲6 | 42 | 100%(n=10) | 100%(n=10) | 90%(n=10) | 通关 |
| 第52局 | 血18/攻6/甲6 | 150 | 100%(n=10) | 100%(n=10) | 90%(n=10) | 通关 |
| 第55局 | 血18/攻6/甲6 | 33 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
| 第58局 | 血18/攻6/甲7 | 141 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
## ④ 换算率标定(OFAT 注入档 vs 白板,同种子配对逐关条件通关率差)
### 生命上限(每级 flat +3)
| 注入档 | 画像 | 层 | 配对局数 | 条件率 白板→档 | 差(点) | 折每级(点/级) |
|------------|--------|--------|----------|----------------|-------------|---------------|
| +3(1 级) | 普通档 | 第一层 | 100 | 53.0→60.0 | +7.0 ±2.6 | +7.0 ±2.6 |
| +3(1 级) | 普通档 | 第二层 | 100 | 1.9→6.7 | +4.8 ±2.8 | +4.8 ±2.8 |
| +3(1 级) | 普通档 | 第三层 | 100 | 0.0→25.0 | +25.0 ±18.4 | +25.0 ±18.4 |
| +3(1 级) | 完美档 | 第一层 | 100 | 86.0→90.0 | +4.0 ±2.0 | +4.0 ±2.0 |
| +3(1 级) | 完美档 | 第二层 | 100 | 17.4→26.7 | +9.2 ±3.2 | +9.2 ±3.2 |
| +3(1 级) | 完美档 | 第三层 | 100 | 0.0→12.5 | +12.5 ±6.7 | +12.5 ±6.7 |
| +6(2 级) | 普通档 | 第一层 | 100 | 53.0→65.0 | +12.0 ±3.2 | +6.0 ±1.6 |
| +6(2 级) | 普通档 | 第二层 | 100 | 1.9→20.0 | +18.1 ±4.9 | +9.1 ±2.4 |
| +6(2 级) | 普通档 | 第三层 | 100 | 0.0→7.7 | +7.7 ±7.1 | +3.8 ±3.6 |
| +6(2 级) | 完美档 | 第一层 | 100 | 86.0→94.0 | +8.0 ±2.7 | +4.0 ±1.3 |
| +6(2 级) | 完美档 | 第二层 | 100 | 17.4→34.0 | +16.6 ±3.9 | +8.3 ±2.0 |
| +6(2 级) | 完美档 | 第三层 | 100 | 0.0→18.8 | +18.8 ±7.1 | +9.4 ±3.5 |
### 基础攻击(每级 flat +1)
| 注入档 | 画像 | 层 | 配对局数 | 条件率 白板→档 | 差(点) | 折每级(点/级) |
|------------|--------|--------|----------|----------------|-------------|---------------|
| +1(1 级) | 普通档 | 第一层 | 100 | 53.0→68.0 | +15.0 ±3.9 | +15.0 ±3.9 |
| +1(1 级) | 普通档 | 第二层 | 100 | 1.9→16.2 | +14.3 ±4.3 | +14.3 ±4.3 |
| +1(1 级) | 普通档 | 第三层 | 100 | 0.0→18.2 | +18.2 ±11.5 | +18.2 ±11.5 |
| +1(1 级) | 完美档 | 第一层 | 100 | 86.0→88.0 | +2.0 ±2.8 | +2.0 ±2.8 |
| +1(1 级) | 完美档 | 第二层 | 100 | 17.4→30.7 | +13.2 ±4.5 | +13.2 ±4.5 |
| +1(1 级) | 完美档 | 第三层 | 100 | 0.0→22.2 | +22.2 ±8.2 | +22.2 ±8.2 |
| +2(2 级) | 普通档 | 第一层 | 100 | 53.0→84.0 | +31.0 ±4.8 | +15.5 ±2.4 |
| +2(2 级) | 普通档 | 第二层 | 100 | 1.9→21.4 | +19.5 ±4.4 | +9.8 ±2.2 |
| +2(2 级) | 普通档 | 第三层 | 100 | 0.0→16.7 | +16.7 ±8.6 | +8.3 ±4.3 |
| +2(2 级) | 完美档 | 第一层 | 100 | 86.0→95.0 | +9.0 ±2.8 | +4.5 ±1.4 |
| +2(2 级) | 完美档 | 第二层 | 100 | 17.4→40.0 | +22.6 ±4.8 | +11.3 ±2.4 |
| +2(2 级) | 完美档 | 第三层 | 100 | 0.0→18.4 | +18.4 ±6.2 | +9.2 ±3.1 |
### 护甲等级(每级 flat +1)
| 注入档 | 画像 | 层 | 配对局数 | 条件率 白板→档 | 差(点) | 折每级(点/级) |
|------------|--------|--------|----------|----------------|-----------|---------------|
| +1(1 级) | 普通档 | 第一层 | 100 | 53.0→53.0 | +0.0 ±0.0 | +0.0 |
| +1(1 级) | 普通档 | 第二层 | 100 | 1.9→1.9 | +0.0 ±0.0 | +0.0 |
| +1(1 级) | 普通档 | 第三层 | 100 | 0.0→0.0 | +0.0 ±0.0 | +0.0 |
| +1(1 级) | 完美档 | 第一层 | 100 | 86.0→89.0 | +3.0 ±1.7 | +3.0 ±1.7 |
| +1(1 级) | 完美档 | 第二层 | 100 | 17.4→18.0 | +0.5 ±1.2 | +0.5 ±1.2 |
| +1(1 级) | 完美档 | 第三层 | 100 | 0.0→0.0 | +0.0 ±0.0 | +0.0 |
| +2(2 级) | 普通档 | 第一层 | 100 | 53.0→55.0 | +2.0 ±1.4 | +1.0 ±0.7 |
| +2(2 级) | 普通档 | 第二层 | 100 | 1.9→3.6 | +1.7 ±1.8 | +0.9 ±0.9 |
| +2(2 级) | 普通档 | 第三层 | 100 | 0.0→0.0 | +0.0 ±0.0 | +0.0 |
| +2(2 级) | 完美档 | 第一层 | 100 | 86.0→89.0 | +3.0 ±1.7 | +1.5 ±0.9 |
| +2(2 级) | 完美档 | 第二层 | 100 | 17.4→20.2 | +2.8 ±2.0 | +1.4 ±1.0 |
| +2(2 级) | 完美档 | 第三层 | 100 | 0.0→0.0 | +0.0 ±0.0 | +0.0 |
### 三行汇总与建议投值
| 属性 | 现行表值(点/级) | 实测每级(点/级, 合并) | 建议投值 | 状态 |
|----------|-----------------|-----------------------|----------|--------------|
| 生命上限 | 6 | +6.0 ±0.7 | 6 | 与表值一致 ✓ |
| 基础攻击 | 9 | +8.8 ±0.8 | 9 | 与表值一致 ✓ |
| 护甲等级 | 1 | +1.2 ±0.4 | 1 | 与表值一致 ✓ |
- 折算语义:每级该属性强化 ≈ 通关率带平移量(百分点)——配对同种子读数,护甲行即解锁后实测(强条目)
- 合并口径:逆方差加权跨(档位×画像×层,配对 bootstrap SE);带平移为线性单值近似——近带顶/带底的读数受天花板压缩,判读看 ④ 明细表
@@ -0,0 +1,39 @@
#!/bin/bash
# 换算率标定批·全量采集脚本(2026-09-23)
# 10 次采集顺序执行:7 个 OFAT 注入档直入口(n=100×双画像×两层,同 seed0 配对)
# + 3 个花费序战役(k=60×双画像)。产物 = production/qa/conversion-calibration/ 下
# 各档 .log/.vjson + 合并 calibration.raw.jsonl(判定端 --from-raw 消费)。
set -u
GODOT=/d/code/godot/Godot_v4.7.2-stable_win64_console.exe
SLICE=/d/code/g-plan2-conversion-calibration/prototypes/dungeon-sweeper-vertical-slice
QA=/d/code/g-plan2-conversion-calibration/production/qa/conversion-calibration
mkdir -p "$QA"
cd "$SLICE" || exit 1
run() {
local name=$1; shift
local t0=$(date +%s)
"$GODOT" --headless --path . --script res://smoke/verifier_batch.gd -- "$@" > "$QA/$name.log" 2>&1
local rc=$?
grep '^VJSON ' "$QA/$name.log" > "$QA/$name.vjson"
local rows=$(wc -l < "$QA/$name.vjson")
local errs=$(grep -cE 'SCRIPT ERROR|Parse Error' "$QA/$name.log")
echo "[$(date +%H:%M:%S)] done $name rc=$rc rows=$rows errs=$errs $(( $(date +%s) - t0 ))s"
}
echo "=== 换算率标定采集开跑 $(date +%F' '%H:%M:%S) ==="
run base n=100 levels=1,2 only=level seed0=8000
run hp3 n=100 levels=1,2 only=level seed0=8000 inject=max_hp:3
run hp6 n=100 levels=1,2 only=level seed0=8000 inject=max_hp:6
run atk1 n=100 levels=1,2 only=level seed0=8000 inject=atk:1
run atk2 n=100 levels=1,2 only=level seed0=8000 inject=atk:2
run ac1 n=100 levels=1,2 only=level seed0=8000 inject=armor_class:1
run ac2 n=100 levels=1,2 only=level seed0=8000 inject=armor_class:2
run camp_hp k=60 only=campaign seed0=8000 spend=hp_first
run camp_atk k=60 only=campaign seed0=8000 spend=atk_first
run camp_ac k=60 only=campaign seed0=8000 spend=armor_first
cat "$QA"/base.vjson "$QA"/hp3.vjson "$QA"/hp6.vjson "$QA"/atk1.vjson "$QA"/atk2.vjson \
"$QA"/ac1.vjson "$QA"/ac2.vjson "$QA"/camp_hp.vjson "$QA"/camp_atk.vjson "$QA"/camp_ac.vjson \
| sed 's/^VJSON //' > "$QA/calibration.raw.jsonl"
echo "=== 合并完成:$(wc -l < "$QA/calibration.raw.jsonl") 局 → $QA/calibration.raw.jsonl ==="
@@ -0,0 +1,40 @@
#!/bin/bash
# 换算率标定批·新口径重测采集脚本(2026-09-23)
# 前提:单关直入口口径退役 + 冒险条件口径生效 + L2 收紧/关间大奖励 10→5 已落 master——
# 首轮(直入口面、旧数值)读数作废,本脚本在冒险条件口径上全量重测。
# 10 次采集顺序执行:白板 + 6 个 OFAT 注入档冒险(na=100×双画像,同 seed0 配对)
# + 3 个花费序战役(k=60×双画像)。产物合并 calibration.raw.jsonl(判定端 --from-raw 消费)。
set -u
GODOT=/d/code/godot/Godot_v4.7.2-stable_win64_console.exe
SLICE=/d/code/g-plan2-conversion-calibration/prototypes/dungeon-sweeper-vertical-slice
QA=/d/code/g-plan2-conversion-calibration/production/qa/conversion-calibration
mkdir -p "$QA"
cd "$SLICE" || exit 1
run() {
local name=$1; shift
local t0=$(date +%s)
"$GODOT" --headless --path . --script res://smoke/verifier_batch.gd -- "$@" > "$QA/$name.log" 2>&1
local rc=$?
grep '^VJSON ' "$QA/$name.log" > "$QA/$name.vjson"
local rows=$(wc -l < "$QA/$name.vjson")
local errs=$(grep -cE 'SCRIPT ERROR|Parse Error' "$QA/$name.log")
echo "[$(date +%H:%M:%S)] done $name rc=$rc rows=$rows errs=$errs $(( $(date +%s) - t0 ))s"
}
echo "=== 新口径重测开跑 $(date +%F' '%H:%M:%S) ==="
run adv_base na=100 only=adventure seed0=8000
run adv_hp3 na=100 only=adventure seed0=8000 inject=max_hp:3
run adv_hp6 na=100 only=adventure seed0=8000 inject=max_hp:6
run adv_atk1 na=100 only=adventure seed0=8000 inject=atk:1
run adv_atk2 na=100 only=adventure seed0=8000 inject=atk:2
run adv_ac1 na=100 only=adventure seed0=8000 inject=armor_class:1
run adv_ac2 na=100 only=adventure seed0=8000 inject=armor_class:2
run camp_hp k=60 only=campaign seed0=8000 spend=hp_first
run camp_atk k=60 only=campaign seed0=8000 spend=atk_first
run camp_ac k=60 only=campaign seed0=8000 spend=armor_first
cat "$QA"/adv_base.vjson "$QA"/adv_hp3.vjson "$QA"/adv_hp6.vjson "$QA"/adv_atk1.vjson "$QA"/adv_atk2.vjson \
"$QA"/adv_ac1.vjson "$QA"/adv_ac2.vjson "$QA"/camp_hp.vjson "$QA"/camp_atk.vjson "$QA"/camp_ac.vjson \
| sed 's/^VJSON //' > "$QA/calibration.raw.jsonl"
echo "=== 合并完成:$(wc -l < "$QA/calibration.raw.jsonl") 局 → $QA/calibration.raw.jsonl ==="
@@ -0,0 +1,33 @@
#!/bin/bash
# 换算率标定批·修复后重采(2026-09-23)
# _defender_ac 护甲断链接线修复后,护甲注入两档与三条战役轨迹(甲轴购买后段生效)需重采;
# base/hp3/hp6/atk1/atk2 五档注入甲=0、修复逐位不变,沿用首轮产物。
set -u
GODOT=/d/code/godot/Godot_v4.7.2-stable_win64_console.exe
SLICE=/d/code/g-plan2-conversion-calibration/prototypes/dungeon-sweeper-vertical-slice
QA=/d/code/g-plan2-conversion-calibration/production/qa/conversion-calibration
mkdir -p "$QA"
cd "$SLICE" || exit 1
run() {
local name=$1; shift
local t0=$(date +%s)
"$GODOT" --headless --path . --script res://smoke/verifier_batch.gd -- "$@" > "$QA/$name.log" 2>&1
local rc=$?
grep '^VJSON ' "$QA/$name.log" > "$QA/$name.vjson"
local rows=$(wc -l < "$QA/$name.vjson")
local errs=$(grep -cE 'SCRIPT ERROR|Parse Error' "$QA/$name.log")
echo "[$(date +%H:%M:%S)] done $name rc=$rc rows=$rows errs=$errs $(( $(date +%s) - t0 ))s"
}
echo "=== 修复后重采开跑 $(date +%F' '%H:%M:%S) ==="
run ac1 n=100 levels=1,2 only=level seed0=8000 inject=armor_class:1
run ac2 n=100 levels=1,2 only=level seed0=8000 inject=armor_class:2
run camp_hp k=60 only=campaign seed0=8000 spend=hp_first
run camp_atk k=60 only=campaign seed0=8000 spend=atk_first
run camp_ac k=60 only=campaign seed0=8000 spend=armor_first
cat "$QA"/base.vjson "$QA"/hp3.vjson "$QA"/hp6.vjson "$QA"/atk1.vjson "$QA"/atk2.vjson \
"$QA"/ac1.vjson "$QA"/ac2.vjson "$QA"/camp_hp.vjson "$QA"/camp_atk.vjson "$QA"/camp_ac.vjson \
| sed 's/^VJSON //' > "$QA/calibration.raw.jsonl"
echo "=== 合并完成:$(wc -l < "$QA/calibration.raw.jsonl") 局 → $QA/calibration.raw.jsonl ==="
@@ -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": 9
}, {
"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,13 @@ 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/9/1 投值。
; 方法 = OFAT 冒险注入档(每属性 +1/+2 级 × 双画像 × na=100,与白板同种子流配对)逐关条件
; 通关率差实测每级带平移 生命 +6.0±0.7 / 攻击 +8.8±0.8 / 护甲 +1.2±0.4 点(配对 bootstrap SE,
; 逆方差加权合并)→ 取整投 6/9/1。测量面 = 冒险条件口径(同日单关直入口口径退役后重测——
; 首轮直入口面读数作废)+ 当日 L2 收紧/关间大奖励 10→5 后的现行数值包。
; 护甲行强条目重测义务随批履行:重测暴露并修复护甲断链(begin_battle hero_armor_class 参数此前
; 零消费——_defender_ac 主角分支本次接线,缺参/-1 = 表值兜底);修复后甲轴读数 +1.2±0.4 为
; 解锁后参数实测,非基线外推。战役轨迹三花费序(生命/攻击/护甲优先)随批采集验证。
@@ -3,16 +3,22 @@
# 2026-09-23 单关直入口口径退役(用户拍板):原 level 段(直入口单关基线——满血零上下文进关,
# 与真实冒险的条件通关率脱节,曾误导曲线调参)整段移除;逐关难度读数由 adventure 段行内
# levels_cleared/stopped_at 派生(判定端 analyze_adventure_levels),零补采。
# 2026-09-23 换算率标定批:① adventure 段增 inject 注入档(永久强化 flat OFAT——标定换算率表的
# 边际读数:每档与白板同种子流配对,逐关条件通关率差 = 该档的带平移量实测);
# ② campaign 段增 armor_first 花费序 + 行带 spend 标签(多轨迹合并采集按序分组)。
# 两段一次采集,stdout 以「VJSON 」前缀行出机器读 JSON(其余行为人读进度/摘要):
# adventure 段 全冒险连探基线(run_adventure 零永久强化)× 双画像 × na——止步分布、败退节点
# (摘要 defeat 明细 + 末场战斗敌方名)与逐关条件通关率派生源
# (摘要 defeat 明细 + 末场战斗敌方名)与逐关条件通关率派生源;inject 档行带
# inject_tag/override(flat 三键)
# campaign 段 营地循环战役 × 双画像 × k 局——SaveManager 隔离档 + MetaProgression 真实账本:
# 逐局 hero_override() 注入 → 冒险 → settle_adventure 入账 → 花费策略买锻造,
# 回答「累计几次通关冒险后下一关可过」(元进度换算率标定的模拟读数源)
# 运行:Godot_console.exe --headless --path . --script res://smoke/verifier_batch.gd -- [k=v ...]
# na=40(基线冒险局数) k=40(战役局数) seed0=8000
# only=adventure,campaign(缺省全段) spend=hp_first|atk_first(锻造花费优先序)
# only=adventure,campaign(缺省全段) spend=hp_first|atk_first|armor_first(锻造花费优先序)
# inject=max_hp:3,atk:1(冒险段 flat 注入档——三键 {max_hp, atk, armor_class},缺省全零白板)
# 种子纪律:unique 逐局独立(掷点测量修复批 A 案——seed0 + 全局游标,每掷点恰消费一次)。
# 同 seed0 跨档复用 = 配对读数(同序种子流——分析侧按种子配对消方差)。
# 日志合规:全段共享 playtest_logger 会话(与真人试玩同日志),分析工具可直接消费。
# 存档红线:战役档用 user://verifier_campaign_<画像>.json 隔离路径,段首清陈旧段尾清理——
# 绝不碰 user://save.json(与 smoke 存档隔离纪律同源)。
@@ -21,7 +27,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 种子全局游标(三段连续消费,段间不重叠)
@@ -51,16 +59,15 @@ func _initialize() -> void:
func _parse_args(args: PackedStringArray) -> Dictionary:
var opt := {"na": 40, "k": 40, "seed0": 8000, "only": "adventure,campaign", "spend": "hp_first"}
var opt := {"na": 40, "k": 40, "seed0": 8000, "only": "adventure,campaign",
"spend": "hp_first", "inject": ""}
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"]:
opt[key] = String(kv[1])
else:
opt[key] = int(kv[1])
@@ -71,9 +78,29 @@ func _parse_args(args: PackedStringArray) -> Dictionary:
opt["only"] = segs
if not SPEND_ORDERS.has(String(opt.spend)):
opt["spend"] = "hp_first"
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
# 注入档人读标签(白板 = "白板";行键 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)
func _next_seed(seed0: int) -> int:
var s: int = seed0 + _cursor
_cursor += 1
@@ -84,16 +111,22 @@ func _emit(row: Dictionary) -> void:
print("VJSON " + JSON.stringify(row))
# ---- adventure 段:全冒险连探基线(零永久强化——止步分布/败退节点归因/逐关条件通关率派生源) ----
# ---- adventure 段:全冒险连探基线(零永久强化——止步分布/败退节点归因/逐关条件通关率派生源);
# inject 档 = 同种子流配对的 OFAT 边际读数(换算率标定批——行带 inject_tag/override flat 三键) ----
func _seg_adventure(db, autoplay, logger, opt: Dictionary, counts: Dictionary) -> void:
var ov: Dictionary = opt.override
var tag := _inject_tag(ov)
for policy in ["normal", "perfect"]:
for i in range(int(opt.na)):
var seed_val := _next_seed(int(opt.seed0))
var st: Dictionary = autoplay.run_adventure(db, seed_val, 0, 0, false, logger, policy, "min")
_emit(_adventure_row(st, policy, seed_val))
var st: Dictionary = autoplay.run_adventure(db, seed_val, 0, 0, false, logger, policy, "min", ov)
var row := _adventure_row(st, policy, seed_val)
row["inject_tag"] = tag
row["override"] = ov.duplicate()
_emit(row)
counts.adventure += 1
print("进度 adventure %s %d/%d" % [policy, int(opt.na), int(opt.na)])
print("进度 adventure %s %d/%d(%s)" % [policy, int(opt.na), int(opt.na), tag])
# 冒险局共用取数行(adventure/campaign 两段同构——summary 缺席 = 编排异常局,原样带 result 供判定端计异常)
@@ -129,6 +162,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():
+353 -77
View File
@@ -3,29 +3,35 @@
r"""数值验证工具(判定端)——自动化评判 + 元进度爬升测量,替代「亲手试玩再反馈」循环。
两段采集(数据源 = smoke/verifier_batch.gd,本文件驱动它并做全部判定):
adventure 段 全冒险连探基线(零永久强化)——逐关条件通关率派生 + 止步分布 + 败退节点归因(层/节点/敌人)
campaign 段 营地循环战役(隔离档真实元进度账本:注入→冒险→收束入账→买锻造)——爬升曲线
adventure 段 全冒险连探基线(零永久强化)——逐关条件通关率派生 + 止步分布 + 败退节点归因(层/节点/敌人);
换算率标定批起支持 inject 注入档(永久强化 flat OFAT——与白板同种子流配对,行带
inject_tag/override,标定面 = 逐关条件通关率差)
campaign 段 营地循环战役(隔离档真实元进度账本:注入→冒险→收束入账→买锻造)——爬升曲线;
行带 spend 花费序标签(多轨迹合并采集按序分组判定)
评判标准(溯源):
① 逐关条件通关率(冒险口径派生——打通 k-1 关才到达第 k 关,通关数/到达数):单关直入口口径
(分带批 2026-09-20,难度曲线与Boss v0.16)满血零上下文进关,与真实冒险的条件通关率脱节,
曾误导曲线调参——已随单关试玩入口一并退役(2026-09-23 用户拍板);旧带值
(L1 普通 [35,55]/L2 普通 [25,45]/完美档 [35,65])锚定直入口读数不可比,
曾误导曲线调参——已随单关试玩入口一并退役(2026-09-23 用户拍板);旧带值锚定直入口读数不可比,
带重锚归曲线调参批——重锚前本表只测不判。
② 元循环目标(用户提案 2026-09-22,数字待确认):累计通过 5 次第 k 层的冒险后,第 k+1 层的
条件通关率(滑窗)应进入该层验收带——判定同①随口径退役暂停(重锚前只给滑窗读数与触发点)。
③ 魔法道具面:道具工坊六件按通关次数解锁(阈值 1/1/2/2/3/3),局内效果未实装(归 Alpha 魔法道具批)——
本工具只报解锁时点,不入判定。
④ 换算率标定(2026-09-23 批新增):OFAT 注入档 vs 白板的同种子配对逐关条件通关率差
→ 每级带平移量点估计±SE(配对 bootstrap)→ 与 meta.tres 现行表值对照、给建议投值。
护甲行强条目:读数必须按解锁后参数实测(本段即重测面),不得用基线外推。
表值硬编码溯源(单一事实源仍在 .tres / GDD,变更须同步):
· 锻造三档 flat(meta.tres):生命 +3 / 攻击 +1 / 护甲 +1
表值溯源(单一事实源 = meta.tres,本文件启动时解析——解析失败即 FAIL,不做静默兜底):
· 锻造三档 flat(forge_products.flat_value)· 换算率三行(conversion.per_level_shift_pct)
· 钥石入账(adventure.tres + meta.tres):每层通关 10 + Boss 首杀/重复杀 5/1 · 8/2 · 12/3
· 工坊阈值(meta.tres):显影尘1 · 工兵令1 · 探宝罗盘2 · 探巢摆钟2 · 地穴全图卷3 · 爆破工兵包3
· 工坊阈值(meta.tres workshop_items):1/1/2/2/3/3(本文件只做中文名映射,不消费数值)
用法:
python tools/balance_verifier.py # 全段默认规模
python tools/balance_verifier.py --quick # 小规模快跑(读数精度低)
python tools/balance_verifier.py --na 60 --k 60 --seed0 9000 --spend atk_first
python tools/balance_verifier.py --only adventure --inject max_hp:3 --na 100
python tools/balance_verifier.py --from-raw <raw.jsonl> # 免重跑,对既有采集重新判定/出报告
输出:控制台判定结论 + production/qa/balance-verifier-<时间戳>.md(报告)+ 同名 .raw.jsonl(原始逐局数据)。
兼容注:2026-09-23 前的旧 raw.jsonl 含 level 段(直入口单关)行——本版判定自动忽略,
@@ -34,6 +40,8 @@ r"""数值验证工具(判定端)——自动化评判 + 元进度爬升测
import argparse
import json
import math
import random
import re
import subprocess
import sys
import time
@@ -50,15 +58,101 @@ ERROR_MARKERS = ("SCRIPT ERROR", "Parse Error")
# ---- 判定标准区(溯源见文件头) ----
# 直入口验收带(原 LEVEL_BANDS)已随单关直入口口径退役(2026-09-23 用户拍板)——
# 逐关/检查点判定全部暂停(judge_band 传 band=None = 只测不判),带重锚归曲线调参批。
# 带重锚后换算率平移随之启用:带按 band_shift_for(override) 平移后套用(表值驱动,④ 段供数)。
CAMPAIGN_CLEARS_TARGET = 5 # 元循环检查点:累计通过第 k 层的冒险次数(用户提案 2026-09-22,待确认)
CAMPAIGN_WINDOW = 10 # 检查点滑窗局数(条件通关率的窗口)
CAMPAIGN_MIN_ATTEMPTS = 5 # 滑窗内少于此局数 = 数据不足(不出判定)
LEVEL_CN = {1: "第一层", 2: "第二层", 3: "第三层"}
NODE_TYPE_CN = {"battle": "战斗", "boss": "Boss", "rest": "恢复", "reward": "奖励", "shop": "商店房"}
FORGE_FLAT = {"max_hp": 3, "atk": 1, "armor_class": 1} # meta.tres 锻造三档每级 flat 值
OVERRIDE_ATTRS = ["max_hp", "atk", "armor_class"] # 注入契约三键(词表同 MetaTable.FORGE_ATTRS)
ATTR_CN = {"max_hp": "生命上限", "atk": "基础攻击", "armor_class": "护甲等级"}
SPEND_CN = {"hp_first": "生命优先", "atk_first": "攻击优先", "armor_first": "护甲优先"}
WORKSHOP_CN = {"probe_dust": "显影尘", "sapper_order": "工兵令", "loot_compass": "探宝罗盘",
"nest_pendulum": "探巢摆钟", "catacomb_scroll": "地穴全图卷", "blast_kit": "爆破工兵包"}
# ---- meta.tres 表值(启动时解析——表值锚配置推导化:判定阈值随配置表走,零硬编码) ----
META_TRES = SLICE / "data" / "tables" / "meta.tres"
FORGE_FLAT = {} # attr → flat_value(锻造三档每级 flat)
CONVERSION = {} # attr → per_level_shift_pct(换算率三行)
def load_meta_table():
"""解析 meta.tres 的 forge_products/conversion 行族(; 注释行剥除 + 括号配平 + 键值扫描)。
解析失败直接 FAIL——静默兜底会复生「过期硬编码常量」bug 类(表值锚配置推导化禁)。
"""
try:
text = META_TRES.read_text(encoding="utf-8")
except OSError as e:
die(f"meta.tres 不可读:{e}")
text = "\n".join(ln for ln in text.splitlines() if not ln.strip().startswith(";"))
for family in ("forge_products", "conversion"):
rows = _parse_tres_rows(text, family)
if not rows:
die(f"meta.tres 行族解析为空:{family}(表结构或解析器失配——人工排查)")
for row in rows:
attr = str(row.get("attr", ""))
if attr not in OVERRIDE_ATTRS:
die(f"meta.tres {family} attr 出域 {OVERRIDE_ATTRS}: {attr}")
if family == "forge_products":
FORGE_FLAT[attr] = int(row.get("flat_value", 0))
else:
CONVERSION[attr] = int(row.get("per_level_shift_pct", 0))
if sorted(FORGE_FLAT) != sorted(OVERRIDE_ATTRS) or sorted(CONVERSION) != sorted(OVERRIDE_ATTRS):
die(f"meta.tres 三键覆盖不全:forge={FORGE_FLAT} conversion={CONVERSION}")
def _parse_tres_rows(text, family):
"""取 family = [ {...}, ... ] 的行字典列表(.tres 人类可读格式——非严格 YAML,同仓惯例不走上解析器)。"""
marker = family + " = ["
start = text.find(marker)
if start < 0:
return []
i = start + len(marker) - 1 # 指向 '['
depth, j = 0, i
while j < len(text):
if text[j] == "[":
depth += 1
elif text[j] == "]":
depth -= 1
if depth == 0:
break
j += 1
body = text[i:j + 1]
rows, depth, row_start = [], 0, None
for idx, ch in enumerate(body):
if ch == "{":
if depth == 0:
row_start = idx
depth += 1
elif ch == "}":
depth -= 1
if depth == 0 and row_start is not None:
block = body[row_start:idx + 1]
row = {}
for key, val in re.findall(r'"(\w+)":\s*(-?\d+|"[^"]*")', block):
row[key] = int(val) if val.lstrip("-").isdigit() else val.strip('"')
rows.append(row)
return rows
def band_shift_for(override):
"""注入/锻造 flat 状态 → 通关率带平移量(百分点)= Σ flat÷每级flat×每级平移(表值驱动)。"""
shift = 0.0
for attr in OVERRIDE_ATTRS:
flat = int((override or {}).get(attr, 0))
per_flat = FORGE_FLAT.get(attr, 1)
if per_flat > 0:
shift += flat / per_flat * CONVERSION.get(attr, 0)
return shift
def shifted_band(band, shift):
"""验收带随平移量平移(clamp [0,100];band=None = 带未定不平移——重锚前恒此态)。"""
if band is None:
return None
return (max(0.0, band[0] + shift), min(100.0, band[1] + shift))
def die(msg):
print(f"[数值验证] FAIL:{msg}")
@@ -81,7 +175,7 @@ def collect(args):
segs = args.only.split(",")
cmd = [args.godot, "--headless", "--path", str(SLICE), "--script", SCRIPT, "--",
f"na={args.na}", f"k={args.k}", f"seed0={args.seed0}",
f"only={args.only}", f"spend={args.spend}"]
f"only={args.only}", f"spend={args.spend}", f"inject={args.inject}"]
print(f"[数值验证] 采集启动({' · '.join(segs)},约 {args.est_min:.0f} 分钟)…")
t0 = time.time()
try:
@@ -140,7 +234,7 @@ def judge_band(rate_pct, band):
return f"越下界(低于 {lo}% 达 {lo - rate_pct:.1f} 点)", False
if rate_pct > hi:
return f"越上界(高于 {hi}% 达 {rate_pct - hi:.1f} 点)", False
return f"落带 [{lo},{hi}] ✓", True
return f"落带 [{lo:g},{hi:g}] ✓", True
def node_label(defeat):
@@ -162,22 +256,24 @@ def node_label(defeat):
# ---------------------------------------------------------------- 分段分析
def valid_adventure_runs(rows, policy):
"""冒险段有效局(summary 在场——levels_cleared >= 0);编排异常局单列。"""
def valid_adventure_runs(rows, policy, baseline_only=False):
"""冒险段有效局(summary 在场——levels_cleared >= 0);编排异常局单列。
baseline_only=True 只留白板档(①② 基线判定面——注入档行归 ④ 标定段)。"""
runs = [r for r in rows if r.get("seg") == "adventure" and r.get("policy") == policy
and int(r.get("levels_cleared", -1)) >= 0]
and int(r.get("levels_cleared", -1)) >= 0
and (not baseline_only or r.get("inject_tag", "白板") == "白板")]
invalid = sum(1 for r in rows if r.get("seg") == "adventure" and r.get("policy") == policy
and int(r.get("levels_cleared", -1)) < 0)
return runs, invalid
def analyze_adventure_levels(rows):
"""逐关条件通关率(冒险口径派生——直入口口径退役 2026-09-23):
"""逐关条件通关率(冒险口径派生——直入口口径退役 2026-09-23;白板档):
到达第 k 关 ⇔ levels_cleared >= k-1;通关第 k 关 ⇔ levels_cleared >= k。
条件通关率 = 通关数 / 到达数(L1 的到达数 = 全部有效局——冒险即从第一关开始)。"""
out = {}
for policy in ("normal", "perfect"):
runs, invalid = valid_adventure_runs(rows, policy)
runs, invalid = valid_adventure_runs(rows, policy, baseline_only=True)
levels = {}
for k in (1, 2, 3):
reached = sum(1 for r in runs if int(r.get("levels_cleared", 0)) >= k - 1)
@@ -190,7 +286,7 @@ def analyze_adventure_levels(rows):
def analyze_adventure(rows):
out = {}
for policy in ("normal", "perfect"):
runs, invalid = valid_adventure_runs(rows, policy)
runs, invalid = valid_adventure_runs(rows, policy, baseline_only=True)
stopped, death_nodes = {}, {}
for r in runs:
sa = int(r.get("stopped_at", -1))
@@ -224,13 +320,25 @@ def window_rate(series, end_idx, window):
return 100.0 * sum(1 for _, ok, _ in chunk if ok) / len(chunk), len(chunk)
def analyze_campaign(rows):
def campaign_spend_tags(rows):
"""战役段花费序标签清单(按出现序;旧采集无 spend 字段 = 单策略,归 hp_first 桶)。"""
tags = []
for r in rows:
if r.get("seg") != "campaign":
continue
t = r.get("spend", "hp_first")
if t not in tags:
tags.append(t)
return tags
def analyze_campaign(rows, spend_tag="hp_first"):
out = {}
for policy in ("normal", "perfect"):
runs = [r for r in rows if r.get("seg") == "campaign" and r.get("policy") == policy
and not r.get("anomaly")]
and r.get("spend", "hp_first") == spend_tag and not r.get("anomaly")]
anomalies = [r for r in rows if r.get("seg") == "campaign" and r.get("policy") == policy
and r.get("anomaly")]
and r.get("spend", "hp_first") == spend_tag and r.get("anomaly")]
res = {"n": len(runs), "anomalies": len(anomalies), "runs": runs}
if not runs:
out[policy] = res
@@ -260,8 +368,10 @@ def analyze_campaign(rows):
rate = 100.0 * sum(1 for _, ok, _ in chunk if ok) / len(chunk)
cp["rate"] = rate
cp["attempts"] = len(chunk)
ov = chunk[0][2].get("override", {})
cp["forge_at"] = ov
cp["band_shift"] = band_shift_for(ov)
cp["verdict"], cp["pass"] = judge_band(rate, None) # 带重锚归曲线调参批
cp["forge_at"] = chunk[0][2].get("override", {})
checkpoints.append(cp)
res["checkpoints"] = checkpoints
# 工坊解锁时点(魔法道具面——只报不判)
@@ -279,6 +389,100 @@ def analyze_campaign(rows):
return out
# ---------------------------------------------------------------- 换算率标定(④,冒险条件口径)
def ofat_cells(rows):
"""OFAT 分组(冒险行):白板基线 {policy: [rows]} 与单轴注入档 {attr: {flat: {policy: [rows]}}}。"""
base, cells = {}, {}
for r in rows:
if r.get("seg") != "adventure" or int(r.get("levels_cleared", -1)) < 0:
continue
ov = r.get("override") or {}
nonzero = [a for a in OVERRIDE_ATTRS if int(ov.get(a, 0)) != 0]
policy = r["policy"]
if not nonzero:
base.setdefault(policy, []).append(r)
elif len(nonzero) == 1:
a = nonzero[0]
cells.setdefault(a, {}).setdefault(int(ov[a]), {}).setdefault(policy, []).append(r)
return base, cells
def cond_rate(rows, k):
"""第 k 关条件通关率(%):通关数/到达数;到达数 0 = None。"""
reached = sum(1 for r in rows if int(r.get("levels_cleared", 0)) >= k - 1)
cleared = sum(1 for r in rows if int(r.get("levels_cleared", 0)) >= k)
if reached <= 0:
return None, reached
return 100.0 * cleared / reached, reached
def paired_cond_delta(base_rows, cell_rows, k, reps=2000):
"""同种子配对第 k 关条件通关率差(点)与配对 bootstrap SE。
点估计 = 全配对样本上 两档条件率之差;SE = 配对 bootstrap(同一次重采样同时进两档——
保种子级相关)。bootsrap 确定性种子固定,复判可重现。
"""
bmap = {int(r["seed"]): r for r in base_rows}
pairs = [(bmap[int(r["seed"])], r) for r in cell_rows if int(r["seed"]) in bmap]
n = len(pairs)
if n == 0:
return None
rate_b, reached_b = cond_rate([p[0] for p in pairs], k)
rate_c, reached_c = cond_rate([p[1] for p in pairs], k)
if rate_b is None or rate_c is None:
return None
rng = random.Random(20260923)
deltas = []
for _ in range(reps):
idx = [rng.randrange(n) for _ in range(n)]
rb, _ = cond_rate([pairs[i][0] for i in idx], k)
rc, _ = cond_rate([pairs[i][1] for i in idx], k)
if rb is not None and rc is not None:
deltas.append(rc - rb)
if not deltas:
return None
se = math.sqrt(sum((d - sum(deltas) / len(deltas)) ** 2 for d in deltas) / (len(deltas) - 1)) if len(deltas) > 1 else 0.0
return {"n": n, "reached_b": reached_b, "reached_c": reached_c,
"rate_b": rate_b, "rate_c": rate_c, "delta": rate_c - rate_b, "se": se}
def calibration_estimate(rows):
"""标定汇总:每属性 → 逐档明细 + 合并每级平移量(逆方差加权)+ 现行表值对照 + 建议投值。"""
base, cells = ofat_cells(rows)
out = {}
for attr, flats in cells.items():
per_flat = max(1, FORGE_FLAT.get(attr, 1))
detail, pts = [], []
for flat in sorted(flats):
lv = flat / per_flat
for policy in sorted(flats[flat]):
for k in (1, 2, 3):
d = paired_cond_delta(base.get(policy, []), flats[flat][policy], k)
if d is None:
continue
per_lvl = d["delta"] / lv
per_lvl_se = d["se"] / lv if d["se"] > 0 else None
detail.append({"flat": flat, "policy": policy, "k": k, "n": d["n"],
"rate_b": d["rate_b"], "rate_c": d["rate_c"],
"delta": d["delta"], "se": d["se"],
"per_lvl": per_lvl, "per_lvl_se": per_lvl_se})
if per_lvl_se is not None and per_lvl_se > 0:
pts.append((per_lvl, 1.0 / per_lvl_se ** 2))
if pts:
pooled = sum(p * w for p, w in pts) / sum(w for _, w in pts)
pooled_se = math.sqrt(1.0 / sum(w for _, w in pts))
elif detail:
pooled = sum(d["per_lvl"] for d in detail) / len(detail)
pooled_se = None
else:
pooled, pooled_se = None, None
sug = None if pooled is None else max(0, min(100, round(pooled)))
out[attr] = {"detail": detail, "pooled": pooled, "pooled_se": pooled_se,
"table_val": CONVERSION.get(attr, 0), "suggest": sug}
return out
# ---------------------------------------------------------------- 报告
def fmt_table(header, rows):
@@ -297,12 +501,14 @@ def report(args, rows, meta):
A(f"# 数值验证报告({meta['ts']})")
A("")
A(f"- 采集规模:基线冒险 {args.na} 局/画像 · 战役 {args.k} 局/画像"
f" · 种子 {args.seed0} 起逐局独立 · 锻造花费策略 = {args.spend}")
f" · 种子 {args.seed0} 起逐局独立 · 锻造花费策略 = {args.spend}"
+ (f" · 注入档 = {args.inject}" if args.inject else ""))
A(f"- 切片版本:{meta['commit']} · 口径:逐关读数 = 冒险条件口径(单关直入口口径已退役 2026-09-23,"
f"带待重锚只测不判——重锚归曲线调参批)")
f"带待重锚只测不判——重锚归曲线调参批) · 换算率表值(meta.tres 启动解析):"
+ " · ".join(f"{ATTR_CN[a]}每级平移{CONVERSION.get(a, 0)}点" for a in OVERRIDE_ATTRS))
A("")
# ① 逐关条件通关率(冒险口径派生)
# ① 逐关条件通关率(冒险口径派生,白板档)
A("## ① 逐关条件通关率(冒险口径——打通 k-1 关才到达第 k 关;带待重锚只测不判)")
A("")
alv = analyze_adventure_levels(rows)
@@ -338,55 +544,111 @@ def report(args, rows, meta):
A(f" - {lb} ×{c}")
A("")
# ③ 元循环爬升
# ③ 元循环爬升(多花费序轨迹分组——换算率标定批起 campaign 行带 spend 标签)
A("## ③ 营地循环爬升(用户判据④:5 次通关 → 下一层可过)")
A("")
camp = analyze_campaign(rows)
for policy in ("normal", "perfect"):
d = camp[policy]
cn = "普通档" if policy == "normal" else "完美档"
A(f"### {cn}({d['n']} 局战役)")
A("")
if not d["runs"]:
A("- 无数据")
for spend in campaign_spend_tags(rows):
camp = analyze_campaign(rows, spend)
spend_cn = SPEND_CN.get(spend, spend)
for policy in ("normal", "perfect"):
d = camp[policy]
cn = "普通档" if policy == "normal" else "完美档"
A(f"### {cn} · {spend_cn}({d['n']} 局战役)")
A("")
continue
A(f"- 钥石:全程入账 {d['income_total']} · 终态余额 {d['keystone_final']}"
f" · 锻造终态 = 生命+{d['forge_final'].get('max_hp', 0) * FORGE_FLAT['max_hp']}"
f"/攻击+{d['forge_final'].get('atk', 0) * FORGE_FLAT['atk']}"
f"/护甲+{d['forge_final'].get('armor_class', 0) * FORGE_FLAT['armor_class']}")
if d["unlocks"]:
A("- 道具工坊解锁时点(魔法道具面,局内效果未实装只报不判):" +
"、".join(f"{k} 第 {v} 局" for k, v in sorted(d["unlocks"].items(), key=lambda x: x[1])))
else:
A("- 道具工坊解锁时点:无(未达阈值)")
if d.get("anomalies"):
A(f"- ⚠ 编排异常局 {d['anomalies']} 局(未结算零入账——timeout/advance_failed,须排查)")
if not d["runs"]:
A("- 无数据")
A("")
continue
A(f"- 钥石:全程入账 {d['income_total']} · 终态余额 {d['keystone_final']}"
f" · 锻造终态 = 生命+{d['forge_final'].get('max_hp', 0) * FORGE_FLAT.get('max_hp', 1)}"
f"/攻击+{d['forge_final'].get('atk', 0) * FORGE_FLAT.get('atk', 1)}"
f"/护甲+{d['forge_final'].get('armor_class', 0) * FORGE_FLAT.get('armor_class', 1)}")
if d["unlocks"]:
A("- 道具工坊解锁时点(魔法道具面,局内效果未实装只报不判):" +
"、".join(f"{k} 第 {v} 局" for k, v in sorted(d["unlocks"].items(), key=lambda x: x[1])))
else:
A("- 道具工坊解锁时点:无(未达阈值)")
if d.get("anomalies"):
A(f"- ⚠ 编排异常局 {d['anomalies']} 局(未结算零入账——timeout/advance_failed,须排查)")
A("")
for cp in d.get("checkpoints", []):
A(f"- 判据:累计通过{LEVEL_CN[cp['level']]} {CAMPAIGN_CLEARS_TARGET} 次后,{LEVEL_CN[cp['level'] + 1]}"
f"条件通关率(滑窗 {CAMPAIGN_WINDOW} 局)——{cp['verdict']}"
+ (f"(触发时注入 = 生命+{cp['forge_at'].get('max_hp', 0)}/攻击+{cp['forge_at'].get('atk', 0)}"
f"/护甲+{cp['forge_at'].get('armor_class', 0)}"
f",表值平移 {cp.get('band_shift', 0.0):+.1f} 点——带重锚后随平移判)"
if "forge_at" in cp else ""))
A("")
tbl = []
series = d.get("series", {})
for r in d["runs"][::max(1, len(d["runs"]) // 20)]:
cells = [f"第{r['run_no']}局",
f"血{r['override'].get('max_hp', 0)}/攻{r['override'].get('atk', 0)}/甲{r['override'].get('armor_class', 0)}",
str(r.get("keystone_after", "-"))]
for k in (1, 2, 3):
s = series.get(k, [])
idx = next((i for i, (rn, _, _) in enumerate(s) if rn == r["run_no"]), None)
if idx is None:
cells.append("—")
else:
rate, cnt = window_rate(s, idx, CAMPAIGN_WINDOW)
cells.append(f"{rate:.0f}%(n={cnt})" if rate is not None else "—")
sa = int(r.get("stopped_at", -1))
cells.append("通关" if sa == 0 else (f"止步第{sa}层" if sa > 0 else str(r.get("result", "?"))))
tbl.append(cells)
A(fmt_table(["局", "注入(flat)", "钥石", "第1层率(窗)", "第2层率(窗)", "第3层率(窗)", "终局"], tbl))
A("")
# ④ 换算率标定(OFAT 注入档配对读数——护甲行强条目重测面;冒险条件口径)
A("## ④ 换算率标定(OFAT 注入档 vs 白板,同种子配对逐关条件通关率差)")
A("")
est = calibration_estimate(rows)
if not any(e["detail"] for e in est.values()):
A("- 无注入档数据(本采集未带 inject 档——标定面跳过)")
A("")
for cp in d.get("checkpoints", []):
A(f"- 判据:累计通过{LEVEL_CN[cp['level']]} {CAMPAIGN_CLEARS_TARGET} 次后,{LEVEL_CN[cp['level'] + 1]}"
f"条件通关率(滑窗 {CAMPAIGN_WINDOW} 局)——{cp['verdict']}"
+ (f"(触发时注入 = 生命+{cp['forge_at'].get('max_hp', 0)}/攻击+{cp['forge_at'].get('atk', 0)}"
f"/护甲+{cp['forge_at'].get('armor_class', 0)})" if "forge_at" in cp else ""))
else:
for attr in OVERRIDE_ATTRS:
e = est.get(attr)
if not e or not e["detail"]:
continue
per_flat = max(1, FORGE_FLAT.get(attr, 1))
A(f"### {ATTR_CN[attr]}(每级 flat +{per_flat})")
A("")
tbl = []
for d in e["detail"]:
cn = "普通档" if d["policy"] == "normal" else "完美档"
tbl.append([
f"+{d['flat']}({d['flat'] // per_flat} 级)", cn, LEVEL_CN[d["k"]],
str(d["n"]), f"{d['rate_b']:.1f}→{d['rate_c']:.1f}",
f"{d['delta']:+.1f} ±{d['se']:.1f}",
f"{d['per_lvl']:+.1f}" + (f" ±{d['per_lvl_se']:.1f}" if d["per_lvl_se"] is not None else ""),
])
A(fmt_table(["注入档", "画像", "层", "配对局数", "条件率 白板→档", "差(点)", "折每级(点/级)"], tbl))
A("")
A("### 三行汇总与建议投值")
A("")
tbl = []
series = d.get("series", {})
for r in d["runs"][::max(1, len(d["runs"]) // 20)]:
cells = [f"第{r['run_no']}局",
f"血{r['override'].get('max_hp', 0)}/攻{r['override'].get('atk', 0)}/甲{r['override'].get('armor_class', 0)}",
str(r.get("keystone_after", "-"))]
for k in (1, 2, 3):
s = series.get(k, [])
idx = next((i for i, (rn, _, _) in enumerate(s) if rn == r["run_no"]), None)
if idx is None:
cells.append("—")
else:
rate, cnt = window_rate(s, idx, CAMPAIGN_WINDOW)
cells.append(f"{rate:.0f}%(n={cnt})" if rate is not None else "—")
sa = int(r.get("stopped_at", -1))
cells.append("通关" if sa == 0 else (f"止步第{sa}层" if sa > 0 else str(r.get("result", "?"))))
tbl.append(cells)
A(fmt_table(["局", "注入(flat)", "钥石", "第1层率(窗)", "第2层率(窗)", "第3层率(窗)", "终局"], tbl))
for attr in OVERRIDE_ATTRS:
e = est.get(attr)
if not e or (not e["detail"] and e["pooled"] is None):
tbl.append([ATTR_CN[attr], str(CONVERSION.get(attr, 0)), "—", "—", "—"])
continue
pooled_s = "—" if e["pooled"] is None else f"{e['pooled']:+.1f}"
if e["pooled"] is not None and e["pooled_se"] is not None:
pooled_s += f" ±{e['pooled_se']:.1f}"
if e["suggest"] is None:
state = "无读数"
elif e["table_val"] == 0:
state = "表值未投值(现行 0)"
elif e["table_val"] == e["suggest"]:
state = "与表值一致 ✓"
else:
state = f"与表值偏差 {e['suggest'] - e['table_val']:+d}"
tbl.append([ATTR_CN[attr], str(e["table_val"]), pooled_s,
"—" if e["suggest"] is None else str(e["suggest"]), state])
A(fmt_table(["属性", "现行表值(点/级)", "实测每级(点/级, 合并)", "建议投值", "状态"], tbl))
A("- 折算语义:每级该属性强化 ≈ 通关率带平移量(百分点)——配对同种子读数,护甲行即解锁后实测(强条目)")
A("- 合并口径:逆方差加权跨(档位×画像×层,配对 bootstrap SE);带平移为线性单值近似——近带顶/带底的读数受天花板压缩,判读看 ④ 明细表")
A("")
return "\n".join(lines)
@@ -412,16 +674,27 @@ def console_verdict(args, rows):
top = sorted(d["death_nodes"].items(), key=lambda x: -x[1])[:2]
print(f" [·] 零强化冒险·{cn}:止步分布 {'、'.join(f'{k}{v}' for k, v in sorted(d['stopped'].items()))}"
+ (f";主要败退点 = {';'.join(f'{lb} ×{c}' for lb, c in top)}" if top else ""))
camp = analyze_campaign(rows)
for policy in ("normal", "perfect"):
d = camp[policy]
cn = "普通档" if policy == "normal" else "完美档"
if not d["runs"]:
print(f" [·] 营地循环·{cn}:无数据")
for spend in campaign_spend_tags(rows):
camp = analyze_campaign(rows, spend)
spend_cn = SPEND_CN.get(spend, spend)
for policy in ("normal", "perfect"):
d = camp[policy]
cn = "普通档" if policy == "normal" else "完美档"
if not d["runs"]:
continue
for cp in d.get("checkpoints", []):
mark = {True: "✓", False: "✗", None: "·"}.get(cp.get("pass"), "·")
print(f" [{mark}] 营地循环·{cn}·{spend_cn}·{LEVEL_CN[cp['level']]}→{LEVEL_CN[cp['level'] + 1]}:{cp['verdict']}")
est = calibration_estimate(rows)
for attr in OVERRIDE_ATTRS:
e = est.get(attr)
if not e or (not e["detail"] and e["pooled"] is None):
continue
for cp in d.get("checkpoints", []):
mark = {True: "✓", False: "✗", None: "·"}.get(cp.get("pass"), "·")
print(f" [{mark}] 营地循环·{cn}·{LEVEL_CN[cp['level']]}→{LEVEL_CN[cp['level'] + 1]}:{cp['verdict']}")
pooled_s = "—" if e["pooled"] is None else f"{e['pooled']:+.1f}"
if e["pooled"] is not None and e["pooled_se"] is not None:
pooled_s += f" ±{e['pooled_se']:.1f}"
print(f" [·] 换算率标定·{ATTR_CN[attr]}:实测每级 {pooled_s} 点 vs 表值 {e['table_val']}"
+ (f" → 建议投 {e['suggest']}" if e["suggest"] is not None else ""))
print()
@@ -436,14 +709,17 @@ def main():
ap.add_argument("--k", type=int, default=60, help="战役局数/画像")
ap.add_argument("--seed0", type=int, default=8000)
ap.add_argument("--only", default="adventure,campaign")
ap.add_argument("--spend", default="hp_first", choices=["hp_first", "atk_first"])
ap.add_argument("--spend", default="hp_first", choices=["hp_first", "atk_first", "armor_first"])
ap.add_argument("--inject", default="", help="冒险段注入档(flat:max_hp:3,atk:1 式——OFAT 标定用)")
ap.add_argument("--timeout", type=int, default=5400)
ap.add_argument("--quick", action="store_true", help="小规模快跑(8/10)")
ap.add_argument("--from-raw", default=None, help="跳过采集,对既有 raw.jsonl 重新判定出报告")
args = ap.parse_args()
if args.quick:
args.na, args.k = 8, 10
args.est_min = (2 * args.na * 3.5 + 2 * args.k * 4.0) / 60
args.est_min = (2 * args.na * 4.0 + 2 * args.k * 4.0) / 60
load_meta_table() # 表值锚配置推导化——解析失败即 FAIL,零硬编码兜底
ts = time.strftime("%Y%m%d-%H%M%S")
if args.from_raw: