换算率标定批: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:
1 parent
3309aae2a3
commit
f0aca09bd0
13 files changed
+3943
-117
No files matched your search
@@ -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/8/1(同批,护甲行强条目重测履行);剩余面 = 定价数字与 Boss 两档
|
||||
|
||||
#### 解锁界面与钥石呈现的交互形态
|
||||
|
||||
|
||||
@@ -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 点** |
|
||||
| 基础攻击强化 | 每级折算为通关率带平移量 | **8 点** |
|
||||
| 护甲等级强化 | 每级折算为通关率带平移量 | **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 / 攻击 8 / 护甲 1 点每级,OFAT 配对读数实测,语义=每级该属性强化折算的通关率带平移百分点);护甲行强条目重测履行并暴露修复护甲断链(begin_battle hero_armor_class 参数零消费→_defender_ac 接线);排期表 P1 行销项 |
|
||||
@@ -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 / 攻击 8 / 护甲 1 点每级——换算率标定批 2026-09-23,OFAT 注入档配对读数实测(每级带平移 +6.5±0.7 / +8.4±0.9 / +1.4±0.4 点取整),护甲行强条目重测随批履行并暴露修复护甲断链(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,16 @@ 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 / 攻击 8 / 护甲 1 点每级
|
||||
# (OFAT 注入档×双画像×L1/L2×n=100 与白板同种子流配对,实测每级带平移 +6.5±0.7 / +8.4±0.9 /
|
||||
# +1.4±0.4 点逆方差合并取整;读数报告 production/qa/conversion-calibration/calibration.raw.md);
|
||||
# ② 护甲行强条目重测履行=重测暴露护甲断链:begin_battle hero_armor_class 参数自元进度实现批
|
||||
# 传入以来零消费(_defender_ac 主角分支只读 db.hero 表值)——永久强化护甲档与冒险加算区
|
||||
# 护甲轴的生效值未进战斗;本批接线(缺参/-1 = 表值兜底,白板路径逐位不变,冒烟 553 项 0 失败
|
||||
# 含 MC 同种子对照闸);修复后甲轴读数 +1.4±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,254 @@
|
||||
# 数值验证报告(20260923-064557)
|
||||
|
||||
- 采集规模:直入口每格 100 局(层集 1,2,3 · 注入档 白板) · 基线冒险 0 局/画像 · 战役 60 局/画像 · 种子 8000 起逐局独立 · 锻造花费策略 = hp_first
|
||||
- 切片版本:3309aae · 换算率表值(meta.tres 启动解析):生命上限每级平移6点 · 基础攻击每级平移8点 · 护甲等级每级平移1点
|
||||
|
||||
## ① 直入口基线通关率(验收带判定,白板档)
|
||||
|
||||
| 层 | 画像 | 通关/局数 | 通关率±95%CI | 验收带 | 判定 |
|
||||
|--------|--------|-----------|--------------|---------|-------------------------------|
|
||||
| 第一层 | 普通档 | 53/100 | 53.0% ±9.8 | [35,55] | 落带 [35,55] ✓ |
|
||||
| 第二层 | 普通档 | 5/100 | 5.0% ±4.3 | [25,45] | 越下界(低于 25% 达 20.0 点) |
|
||||
| 第一层 | 完美档 | 82/100 | 82.0% ±7.5 | [35,65] | 越上界(高于 65% 达 17.0 点) |
|
||||
| 第二层 | 完美档 | 21/100 | 21.0% ±8.0 | [35,65] | 越下界(低于 35% 达 14.0 点) |
|
||||
|
||||
## ② 零永久强化冒险:止步分布与败退节点(用户判据③)
|
||||
|
||||
## ③ 营地循环爬升(用户判据④:5 次通关 → 下一层可过)
|
||||
|
||||
### 普通档 · 生命优先(60 局战役)
|
||||
|
||||
- 钥石:全程入账 1440 · 终态余额 204 · 锻造终态 = 生命+18/攻击+6/护甲+6
|
||||
- 道具工坊解锁时点(魔法道具面,局内效果未实装只报不判):显影尘 第 3 局、工兵令 第 3 局、探宝罗盘 第 5 局、探巢摆钟 第 5 局、地穴全图卷 第 6 局、爆破工兵包 第 6 局
|
||||
|
||||
- 判据:累计通过第一层 5 次后,第二层条件通关率(滑窗 10 局)——越下界(低于 45.0% 达 5.0 点)(触发时注入 = 生命+6/攻击+1/护甲+0,表值平移 +20.0 点)
|
||||
- 判据:累计通过第二层 5 次后,第三层条件通关率(滑窗 10 局)——带未定(只测不判)(触发时注入 = 生命+9/攻击+3/护甲+1,表值平移 +43.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 | 16 | 40%(n=10) | 25%(n=4) | 0%(n=1) | 止步第3层 |
|
||||
| 第13局 | 血6/攻0/甲0 | 16 | 30%(n=10) | — | — | 止步第1层 |
|
||||
| 第16局 | 血6/攻1/甲0 | 18 | 30%(n=10) | 17%(n=6) | — | 止步第2层 |
|
||||
| 第19局 | 血6/攻1/甲1 | 13 | 60%(n=10) | 22%(n=9) | 0%(n=2) | 止步第3层 |
|
||||
| 第22局 | 血9/攻2/甲1 | 13 | 80%(n=10) | 30%(n=10) | — | 止步第2层 |
|
||||
| 第25局 | 血9/攻3/甲1 | 25 | 100%(n=10) | 40%(n=10) | 0%(n=5) | 止步第3层 |
|
||||
| 第28局 | 血12/攻3/甲1 | 20 | 100%(n=10) | 60%(n=10) | 29%(n=7) | 通关 |
|
||||
| 第31局 | 血12/攻3/甲3 | 16 | 100%(n=10) | 70%(n=10) | 50%(n=10) | 通关 |
|
||||
| 第34局 | 血12/攻4/甲3 | 31 | 100%(n=10) | 90%(n=10) | 60%(n=10) | 通关 |
|
||||
| 第37局 | 血15/攻4/甲4 | 39 | 100%(n=10) | 100%(n=10) | 80%(n=10) | 通关 |
|
||||
| 第40局 | 血15/攻5/甲4 | 47 | 100%(n=10) | 100%(n=10) | 80%(n=10) | 通关 |
|
||||
| 第43局 | 血15/攻5/甲5 | 55 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第46局 | 血15/攻5/甲5 | 0 | 100%(n=10) | 100%(n=10) | 90%(n=10) | 止步第3层 |
|
||||
| 第49局 | 血18/攻5/甲5 | 108 | 100%(n=10) | 100%(n=10) | 90%(n=10) | 通关 |
|
||||
| 第52局 | 血18/攻6/甲5 | 66 | 100%(n=10) | 100%(n=10) | 90%(n=10) | 通关 |
|
||||
| 第55局 | 血18/攻6/甲5 | 24 | 100%(n=10) | 100%(n=10) | 90%(n=10) | 通关 |
|
||||
| 第58局 | 血18/攻6/甲6 | 132 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
|
||||
### 完美档 · 生命优先(60 局战役)
|
||||
|
||||
- 钥石:全程入账 2026 · 终态余额 115 · 锻造终态 = 生命+21/攻击+7/护甲+7
|
||||
- 道具工坊解锁时点(魔法道具面,局内效果未实装只报不判):显影尘 第 1 局、工兵令 第 1 局、探宝罗盘 第 2 局、探巢摆钟 第 2 局、地穴全图卷 第 3 局、爆破工兵包 第 3 局
|
||||
|
||||
- 判据:累计通过第一层 5 次后,第二层条件通关率(滑窗 10 局)——越上界(高于 85.0% 达 15.0 点)(触发时注入 = 生命+6/攻击+1/护甲+0,表值平移 +20.0 点)
|
||||
- 判据:累计通过第二层 5 次后,第三层条件通关率(滑窗 10 局)——带未定(只测不判)(触发时注入 = 生命+6/攻击+2/护甲+2,表值平移 +30.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 | 15 | 100%(n=7) | 57%(n=7) | 0%(n=4) | 止步第3层 |
|
||||
| 第10局 | 血9/攻2/甲2 | 31 | 100%(n=10) | 70%(n=10) | 14%(n=7) | 止步第3层 |
|
||||
| 第13局 | 血12/攻2/甲2 | 27 | 100%(n=10) | 90%(n=10) | 40%(n=10) | 通关 |
|
||||
| 第16局 | 血12/攻3/甲3 | 23 | 100%(n=10) | 100%(n=10) | 70%(n=10) | 通关 |
|
||||
| 第19局 | 血12/攻4/甲4 | 64 | 100%(n=10) | 100%(n=10) | 90%(n=10) | 通关 |
|
||||
| 第22局 | 血15/攻4/甲4 | 72 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第25局 | 血15/攻5/甲4 | 80 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第28局 | 血15/攻5/甲5 | 88 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第31局 | 血18/攻5/甲5 | 46 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第34局 | 血18/攻5/甲5 | 4 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第37局 | 血18/攻6/甲5 | 112 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第40局 | 血18/攻6/甲6 | 70 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第43局 | 血18/攻6/甲6 | 178 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第46局 | 血21/攻6/甲6 | 61 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第49局 | 血21/攻6/甲6 | 169 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第52局 | 血21/攻7/甲6 | 52 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第55局 | 血21/攻7/甲6 | 160 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第58局 | 血21/攻7/甲7 | 43 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
|
||||
### 普通档 · 攻击优先(60 局战役)
|
||||
|
||||
- 钥石:全程入账 1615 · 终态余额 154 · 锻造终态 = 生命+18/攻击+7/护甲+6
|
||||
- 道具工坊解锁时点(魔法道具面,局内效果未实装只报不判):显影尘 第 3 局、工兵令 第 3 局、探宝罗盘 第 5 局、探巢摆钟 第 5 局、地穴全图卷 第 6 局、爆破工兵包 第 6 局
|
||||
|
||||
- 判据:累计通过第一层 5 次后,第二层条件通关率(滑窗 10 局)——越上界(高于 67.0% 达 3.0 点)(触发时注入 = 生命+3/攻击+2/护甲+0,表值平移 +22.0 点)
|
||||
- 判据:累计通过第二层 5 次后,第三层条件通关率(滑窗 10 局)——带未定(只测不判)(触发时注入 = 生命+6/攻击+3/护甲+2,表值平移 +38.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 | 19 | 40%(n=10) | 40%(n=5) | 0%(n=2) | 止步第3层 |
|
||||
| 第16局 | 血3/攻3/甲0 | 0 | 40%(n=10) | 43%(n=7) | — | 止步第2层 |
|
||||
| 第19局 | 血6/攻3/甲1 | 7 | 70%(n=10) | 50%(n=10) | 20%(n=5) | 止步第3层 |
|
||||
| 第22局 | 血9/攻3/甲2 | 12 | 90%(n=10) | 80%(n=10) | 38%(n=8) | 通关 |
|
||||
| 第25局 | 血9/攻3/甲3 | 28 | 100%(n=10) | 70%(n=10) | 50%(n=10) | 通关 |
|
||||
| 第28局 | 血12/攻4/甲3 | 44 | 100%(n=10) | 80%(n=10) | 70%(n=10) | 通关 |
|
||||
| 第31局 | 血12/攻4/甲4 | 85 | 100%(n=10) | 80%(n=10) | 90%(n=10) | 通关 |
|
||||
| 第34局 | 血12/攻5/甲4 | 93 | 100%(n=10) | 90%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第37局 | 血15/攻5/甲4 | 1 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第40局 | 血15/攻5/甲5 | 109 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第43局 | 血15/攻6/甲5 | 67 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第46局 | 血15/攻6/甲5 | 25 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第49局 | 血18/攻6/甲5 | 133 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第52局 | 血18/攻6/甲6 | 91 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第55局 | 血18/攻6/甲6 | 199 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第58局 | 血18/攻7/甲6 | 82 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
|
||||
### 完美档 · 攻击优先(60 局战役)
|
||||
|
||||
- 钥石:全程入账 2064 · 终态余额 153 · 锻造终态 = 生命+21/攻击+7/护甲+7
|
||||
- 道具工坊解锁时点(魔法道具面,局内效果未实装只报不判):显影尘 第 1 局、工兵令 第 1 局、探宝罗盘 第 2 局、探巢摆钟 第 2 局、地穴全图卷 第 3 局、爆破工兵包 第 3 局
|
||||
|
||||
- 判据:累计通过第一层 5 次后,第二层条件通关率(滑窗 10 局)——越上界(高于 94.0% 达 6.0 点)(触发时注入 = 生命+6/攻击+2/护甲+1,表值平移 +29.0 点)
|
||||
- 判据:累计通过第二层 5 次后,第三层条件通关率(滑窗 10 局)——带未定(只测不判)(触发时注入 = 生命+6/攻击+2/护甲+2,表值平移 +30.0 点)
|
||||
|
||||
| 局 | 注入(flat) | 钥石 | 第1层率(窗) | 第2层率(窗) | 第3层率(窗) | 终局 |
|
||||
|--------|--------------|------|-------------|-------------|-------------|-----------|
|
||||
| 第1局 | 血0/攻0/甲0 | 15 | 100%(n=1) | 0%(n=1) | — | 止步第2层 |
|
||||
| 第4局 | 血3/攻2/甲0 | 0 | 100%(n=4) | 50%(n=4) | 50%(n=2) | 止步第3层 |
|
||||
| 第7局 | 血6/攻2/甲2 | 32 | 100%(n=7) | 71%(n=7) | 40%(n=5) | 止步第3层 |
|
||||
| 第10局 | 血9/攻3/甲2 | 24 | 100%(n=10) | 80%(n=10) | 38%(n=8) | 止步第3层 |
|
||||
| 第13局 | 血9/攻3/甲3 | 20 | 100%(n=10) | 100%(n=10) | 50%(n=10) | 通关 |
|
||||
| 第16局 | 血12/攻4/甲3 | 61 | 100%(n=10) | 100%(n=10) | 70%(n=10) | 通关 |
|
||||
| 第19局 | 血12/攻4/甲4 | 2 | 100%(n=10) | 100%(n=10) | 90%(n=10) | 通关 |
|
||||
| 第22局 | 血12/攻5/甲4 | 10 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第25局 | 血15/攻5/甲4 | 18 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第28局 | 血15/攻5/甲5 | 126 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第31局 | 血15/攻6/甲5 | 84 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第34局 | 血18/攻6/甲5 | 42 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第37局 | 血18/攻6/甲5 | 0 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第40局 | 血18/攻6/甲6 | 108 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第43局 | 血18/攻6/甲6 | 216 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第46局 | 血18/攻7/甲6 | 99 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第49局 | 血18/攻7/甲6 | 207 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第52局 | 血21/攻7/甲6 | 90 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第55局 | 血21/攻7/甲6 | 198 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第58局 | 血21/攻7/甲7 | 81 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
|
||||
### 普通档 · 护甲优先(60 局战役)
|
||||
|
||||
- 钥石:全程入账 1404 · 终态余额 168 · 锻造终态 = 生命+18/攻击+6/护甲+6
|
||||
- 道具工坊解锁时点(魔法道具面,局内效果未实装只报不判):显影尘 第 3 局、工兵令 第 3 局、探宝罗盘 第 5 局、探巢摆钟 第 5 局、地穴全图卷 第 6 局、爆破工兵包 第 6 局
|
||||
|
||||
- 判据:累计通过第一层 5 次后,第二层条件通关率(滑窗 10 局)——落带 [33.0,53.0] ✓(触发时注入 = 生命+3/攻击+0/护甲+2,表值平移 +8.0 点)
|
||||
- 判据:累计通过第二层 5 次后,第三层条件通关率(滑窗 10 局)——带未定(只测不判)(触发时注入 = 生命+9/攻击+2/护甲+3,表值平移 +37.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 | 16 | 40%(n=10) | 25%(n=4) | 0%(n=1) | 止步第3层 |
|
||||
| 第13局 | 血0/攻0/甲2 | 16 | 30%(n=10) | — | — | 止步第1层 |
|
||||
| 第16局 | 血3/攻0/甲2 | 18 | 30%(n=10) | 17%(n=6) | — | 止步第2层 |
|
||||
| 第19局 | 血3/攻1/甲2 | 13 | 60%(n=10) | 22%(n=9) | 0%(n=2) | 止步第3层 |
|
||||
| 第22局 | 血6/攻1/甲2 | 5 | 70%(n=10) | 20%(n=10) | — | 止步第2层 |
|
||||
| 第25局 | 血6/攻2/甲2 | 17 | 90%(n=10) | 30%(n=10) | 0%(n=4) | 止步第3层 |
|
||||
| 第28局 | 血9/攻2/甲3 | 6 | 90%(n=10) | 50%(n=10) | 17%(n=6) | 止步第3层 |
|
||||
| 第31局 | 血9/攻3/甲4 | 47 | 100%(n=10) | 70%(n=10) | 44%(n=9) | 通关 |
|
||||
| 第34局 | 血12/攻3/甲4 | 8 | 100%(n=10) | 90%(n=10) | 60%(n=10) | 通关 |
|
||||
| 第37局 | 血12/攻4/甲4 | 16 | 100%(n=10) | 100%(n=10) | 80%(n=10) | 通关 |
|
||||
| 第40局 | 血12/攻4/甲5 | 24 | 100%(n=10) | 100%(n=10) | 90%(n=10) | 通关 |
|
||||
| 第43局 | 血15/攻4/甲5 | 32 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第46局 | 血15/攻5/甲5 | 127 | 100%(n=10) | 100%(n=10) | 90%(n=10) | 止步第3层 |
|
||||
| 第49局 | 血15/攻5/甲6 | 85 | 100%(n=10) | 100%(n=10) | 90%(n=10) | 通关 |
|
||||
| 第52局 | 血18/攻5/甲6 | 43 | 100%(n=10) | 100%(n=10) | 90%(n=10) | 通关 |
|
||||
| 第55局 | 血18/攻5/甲6 | 138 | 100%(n=10) | 100%(n=10) | 80%(n=10) | 止步第3层 |
|
||||
| 第58局 | 血18/攻6/甲6 | 96 | 100%(n=10) | 100%(n=10) | 90%(n=10) | 通关 |
|
||||
|
||||
### 完美档 · 护甲优先(60 局战役)
|
||||
|
||||
- 钥石:全程入账 1977 · 终态余额 66 · 锻造终态 = 生命+21/攻击+7/护甲+7
|
||||
- 道具工坊解锁时点(魔法道具面,局内效果未实装只报不判):显影尘 第 1 局、工兵令 第 1 局、探宝罗盘 第 2 局、探巢摆钟 第 2 局、地穴全图卷 第 3 局、爆破工兵包 第 3 局
|
||||
|
||||
- 判据:累计通过第一层 5 次后,第二层条件通关率(滑窗 10 局)——落带 [43.0,73.0] ✓(触发时注入 = 生命+3/攻击+0/护甲+2,表值平移 +8.0 点)
|
||||
- 判据:累计通过第二层 5 次后,第三层条件通关率(滑窗 10 局)——带未定(只测不判)(触发时注入 = 生命+9/攻击+1/护甲+3,表值平移 +29.0 点)
|
||||
|
||||
| 局 | 注入(flat) | 钥石 | 第1层率(窗) | 第2层率(窗) | 第3层率(窗) | 终局 |
|
||||
|--------|--------------|------|-------------|-------------|-------------|-----------|
|
||||
| 第1局 | 血0/攻0/甲0 | 15 | 100%(n=1) | 0%(n=1) | — | 止步第2层 |
|
||||
| 第4局 | 血0/攻0/甲2 | 16 | 100%(n=4) | 25%(n=4) | — | 止步第2层 |
|
||||
| 第7局 | 血6/攻0/甲2 | 11 | 100%(n=7) | 29%(n=7) | — | 止步第2层 |
|
||||
| 第10局 | 血6/攻1/甲3 | 25 | 100%(n=10) | 40%(n=10) | 25%(n=4) | 止步第3层 |
|
||||
| 第13局 | 血9/攻2/甲3 | 0 | 100%(n=10) | 60%(n=10) | 43%(n=7) | 通关 |
|
||||
| 第16局 | 血9/攻3/甲4 | 41 | 100%(n=10) | 80%(n=10) | 60%(n=10) | 通关 |
|
||||
| 第19局 | 血12/攻3/甲4 | 15 | 100%(n=10) | 100%(n=10) | 80%(n=10) | 通关 |
|
||||
| 第22局 | 血12/攻4/甲4 | 23 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第25局 | 血12/攻4/甲5 | 31 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第28局 | 血15/攻5/甲5 | 39 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第31局 | 血15/攻5/甲5 | 147 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第34局 | 血15/攻5/甲6 | 105 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第37局 | 血18/攻5/甲6 | 63 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第40局 | 血18/攻5/甲6 | 21 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第43局 | 血18/攻6/甲6 | 129 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第46局 | 血18/攻6/甲6 | 12 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第49局 | 血18/攻6/甲7 | 120 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第52局 | 血18/攻6/甲7 | 3 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第55局 | 血21/攻6/甲7 | 111 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
| 第58局 | 血21/攻6/甲7 | 219 | 100%(n=10) | 100%(n=10) | 100%(n=10) | 通关 |
|
||||
|
||||
## ④ 换算率标定(OFAT 注入档 vs 白板,同种子配对)
|
||||
|
||||
### 生命上限(每级 flat +3)
|
||||
|
||||
| 注入档 | 画像 | 层 | 配对局数 | 通关率差(点) | 折每级(点/级) |
|
||||
|------------|--------|--------|----------|--------------|---------------|
|
||||
| +3(1 级) | 普通档 | 第一层 | 100 | +7.0 ±2.6 | +7.0 ±2.6 |
|
||||
| +3(1 级) | 普通档 | 第二层 | 100 | +5.0 ±2.2 | +5.0 ±2.2 |
|
||||
| +3(1 级) | 完美档 | 第一层 | 100 | +9.0 ±2.9 | +9.0 ±2.9 |
|
||||
| +3(1 级) | 完美档 | 第二层 | 100 | +6.0 ±2.4 | +6.0 ±2.4 |
|
||||
| +6(2 级) | 普通档 | 第一层 | 100 | +12.0 ±3.3 | +6.0 ±1.6 |
|
||||
| +6(2 级) | 普通档 | 第二层 | 100 | +11.0 ±3.1 | +5.5 ±1.6 |
|
||||
| +6(2 级) | 完美档 | 第一层 | 100 | +14.0 ±3.5 | +7.0 ±1.7 |
|
||||
| +6(2 级) | 完美档 | 第二层 | 100 | +16.0 ±3.7 | +8.0 ±1.8 |
|
||||
|
||||
### 基础攻击(每级 flat +1)
|
||||
|
||||
| 注入档 | 画像 | 层 | 配对局数 | 通关率差(点) | 折每级(点/级) |
|
||||
|------------|--------|--------|----------|--------------|---------------|
|
||||
| +1(1 级) | 普通档 | 第一层 | 100 | +15.0 ±3.9 | +15.0 ±3.9 |
|
||||
| +1(1 级) | 普通档 | 第二层 | 100 | +9.0 ±3.2 | +9.0 ±3.2 |
|
||||
| +1(1 级) | 完美档 | 第一层 | 100 | +9.0 ±2.9 | +9.0 ±2.9 |
|
||||
| +1(1 级) | 完美档 | 第二层 | 100 | +2.0 ±3.5 | +2.0 ±3.5 |
|
||||
| +2(2 级) | 普通档 | 第一层 | 100 | +31.0 ±4.9 | +15.5 ±2.4 |
|
||||
| +2(2 级) | 普通档 | 第二层 | 100 | +24.0 ±4.3 | +12.0 ±2.1 |
|
||||
| +2(2 级) | 完美档 | 第一层 | 100 | +7.0 ±3.3 | +3.5 ±1.6 |
|
||||
| +2(2 级) | 完美档 | 第二层 | 100 | +15.0 ±4.4 | +7.5 ±2.2 |
|
||||
|
||||
### 护甲等级(每级 flat +1)
|
||||
|
||||
| 注入档 | 画像 | 层 | 配对局数 | 通关率差(点) | 折每级(点/级) |
|
||||
|------------|--------|--------|----------|--------------|---------------|
|
||||
| +1(1 级) | 普通档 | 第一层 | 100 | +0.0 ±0.0 | +0.0 ±0.0 |
|
||||
| +1(1 级) | 普通档 | 第二层 | 100 | +0.0 ±0.0 | +0.0 ±0.0 |
|
||||
| +1(1 级) | 完美档 | 第一层 | 100 | +3.0 ±1.7 | +3.0 ±1.7 |
|
||||
| +1(1 级) | 完美档 | 第二层 | 100 | +0.0 ±0.0 | +0.0 ±0.0 |
|
||||
| +2(2 级) | 普通档 | 第一层 | 100 | +2.0 ±1.4 | +1.0 ±0.7 |
|
||||
| +2(2 级) | 普通档 | 第二层 | 100 | +2.0 ±1.4 | +1.0 ±0.7 |
|
||||
| +2(2 级) | 完美档 | 第一层 | 100 | +6.0 ±2.4 | +3.0 ±1.2 |
|
||||
| +2(2 级) | 完美档 | 第二层 | 100 | +0.0 ±0.0 | +0.0 ±0.0 |
|
||||
|
||||
### 三行汇总与建议投值
|
||||
|
||||
| 属性 | 现行表值(点/级) | 实测每级(点/级, 合并) | 建议投值 | 状态 |
|
||||
|----------|-----------------|-----------------------|----------|--------------|
|
||||
| 生命上限 | 6 | +6.5 ±0.7 | 6 | 与表值一致 ✓ |
|
||||
| 基础攻击 | 8 | +8.4 ±0.9 | 8 | 与表值一致 ✓ |
|
||||
| 护甲等级 | 1 | +1.4 ±0.4 | 1 | 与表值一致 ✓ |
|
||||
- 折算语义:每级该属性强化 ≈ 通关率带平移量(百分点)——配对同种子读数,护甲行即解锁后实测(强条目)
|
||||
- 合并口径:逆方差加权跨(档位×画像×层);带平移为线性单值近似——近带顶/带底的读数受天花板压缩,判读看 ④ 明细表
|
||||
@@ -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,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": 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():
|
||||
|
||||
+351
-82
@@ -3,34 +3,43 @@
|
||||
r"""数值验证工具(判定端)——自动化评判 + 元进度爬升测量,替代「亲手试玩再反馈」循环。
|
||||
|
||||
三段采集(数据源 = smoke/verifier_batch.gd,本文件驱动它并做全部判定):
|
||||
level 段 直入口单关基线(零加成)——验收带判定
|
||||
level 段 直入口单关基线(零加成)——验收带判定;换算率标定批起支持 inject 注入档
|
||||
(永久强化 flat OFAT——同种子流配对读数,通关率差 = 带平移量实测)
|
||||
adventure 段 全冒险连探基线(零永久强化)——止步分布 + 败退节点归因(层/节点/敌人)
|
||||
campaign 段 营地循环战役(隔离档真实元进度账本:注入→冒险→收束入账→买锻造)——爬升曲线
|
||||
campaign 段 营地循环战役(隔离档真实元进度账本:注入→冒险→收束入账→买锻造)——爬升曲线;
|
||||
换算率标定批起行带 spend 花费序标签(多轨迹合并采集按序分组判定)
|
||||
|
||||
评判标准(溯源):
|
||||
① 直入口基线通关率带(分带批 2026-09-20 用户拍板,难度曲线与Boss v0.16):
|
||||
第一层 普通 [35,55]% · 第二层 普通 [25,45]% · 完美档 [35,65]%(两关同带);第三层带未定——只测不判。
|
||||
带平移(元进度定稿批口径):注入/锻造状态按 meta.tres 换算率表折算平移量后套用——
|
||||
判读口径不变、带随解锁平移(表值驱动,本文件解析 meta.tres 取数,零硬编码)。
|
||||
② 元循环目标(用户提案 2026-09-22,数字待确认):累计通过 5 次第 k 层的冒险后,第 k+1 层的
|
||||
条件通关率(滑窗)应进入该层验收带——「5 次通过第一层的冒险后能过第二层;5 次通过第二层后能过第三层」。
|
||||
第三层带未定,只给爬升曲线与首次读数,不出判定。
|
||||
③ 魔法道具面:道具工坊六件按通关次数解锁(阈值 1/1/2/2/3/3),局内效果未实装(归 Alpha 魔法道具批)——
|
||||
本工具只报解锁时点,不入判定。
|
||||
④ 换算率标定(2026-09-23 批新增):OFAT 注入档 vs 白板的配对通关率差 → 每级平移量点估计±SE
|
||||
→ 与 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 # 全段默认规模(约 20–35 分钟)
|
||||
python tools/balance_verifier.py --quick # 小规模快跑(约 5–10 分钟,读数精度低)
|
||||
python tools/balance_verifier.py --n 100 --na 60 --k 60 --seed0 9000 --spend atk_first
|
||||
python tools/balance_verifier.py --only level --levels 1,2 --inject max_hp:3 --n 100
|
||||
python tools/balance_verifier.py --from-raw <raw.jsonl> # 免重跑,对既有采集重新判定/出报告
|
||||
输出:控制台判定结论 + production/qa/balance-verifier-<时间戳>.md(报告)+ 同名 .raw.jsonl(原始逐局数据)。
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
@@ -56,9 +65,93 @@ 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)
|
||||
WORKSHOP_CN = {"probe_dust": "显影尘", "sapper_order": "工兵令", "loot_compass": "探宝罗盘",
|
||||
"nest_pendulum": "探巢摆钟", "catacomb_scroll": "地穴全图卷", "blast_kit": "爆破工兵包"}
|
||||
SPEND_CN = {"hp_first": "生命优先", "atk_first": "攻击优先", "armor_first": "护甲优先"}
|
||||
|
||||
# ---- 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):
|
||||
@@ -82,7 +175,8 @@ def collect(args):
|
||||
segs = args.only.split(",")
|
||||
cmd = [args.godot, "--headless", "--path", str(SLICE), "--script", SCRIPT, "--",
|
||||
f"n={args.n}", 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}", f"levels={args.levels}"]
|
||||
print(f"[数值验证] 采集启动({' · '.join(segs)},约 {args.est_min} 分钟)…")
|
||||
t0 = time.time()
|
||||
try:
|
||||
@@ -163,14 +257,17 @@ def node_label(defeat):
|
||||
|
||||
# ---------------------------------------------------------------- 分段分析
|
||||
|
||||
def analyze_level(rows):
|
||||
"""直入口基线:按(画像, 层)聚合计数。"""
|
||||
def analyze_level(rows, baseline_only=False):
|
||||
"""直入口基线:按(画像, 层, 注入档)聚合计数。baseline_only=True 只留白板档(① 基线判定面)。"""
|
||||
agg = {}
|
||||
for r in rows:
|
||||
if r.get("seg") != "level":
|
||||
continue
|
||||
key = (r["policy"], int(r["level"]))
|
||||
a = agg.setdefault(key, {"n": 0, "wins": 0, "deaths": {}})
|
||||
tag = r.get("inject_tag", "白板")
|
||||
if baseline_only and tag != "白板":
|
||||
continue
|
||||
key = (r["policy"], int(r["level"]), tag)
|
||||
a = agg.setdefault(key, {"n": 0, "wins": 0, "deaths": {}, "override": r.get("override") or {}})
|
||||
a["n"] += 1
|
||||
if r.get("result") == "victory":
|
||||
a["wins"] += 1
|
||||
@@ -221,13 +318,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
|
||||
@@ -257,9 +366,11 @@ def analyze_campaign(rows):
|
||||
rate = 100.0 * sum(1 for _, ok, _ in chunk if ok) / len(chunk)
|
||||
cp["rate"] = rate
|
||||
cp["attempts"] = len(chunk)
|
||||
band = LEVEL_BANDS.get((policy, k + 1))
|
||||
ov = chunk[0][2].get("override", {})
|
||||
cp["forge_at"] = ov
|
||||
cp["band_shift"] = band_shift_for(ov)
|
||||
band = shifted_band(LEVEL_BANDS.get((policy, k + 1)), cp["band_shift"])
|
||||
cp["verdict"], cp["pass"] = judge_band(rate, band)
|
||||
cp["forge_at"] = chunk[0][2].get("override", {})
|
||||
checkpoints.append(cp)
|
||||
res["checkpoints"] = checkpoints
|
||||
# 工坊解锁时点(魔法道具面——只报不判)
|
||||
@@ -277,6 +388,81 @@ def analyze_campaign(rows):
|
||||
return out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 换算率标定(④)
|
||||
|
||||
def ofat_cells(rows):
|
||||
"""OFAT 分组:白板基线 {(policy, level): [rows]} 与单轴注入档 {attr: {flat: {(policy, level): [rows]}}}。"""
|
||||
base, cells = {}, {}
|
||||
for r in rows:
|
||||
if r.get("seg") != "level":
|
||||
continue
|
||||
ov = r.get("override") or {}
|
||||
nonzero = [a for a in OVERRIDE_ATTRS if int(ov.get(a, 0)) != 0]
|
||||
key = (r["policy"], int(r["level"]))
|
||||
if not nonzero:
|
||||
base.setdefault(key, []).append(r)
|
||||
elif len(nonzero) == 1:
|
||||
a = nonzero[0]
|
||||
cells.setdefault(a, {}).setdefault(int(ov[a]), {}).setdefault(key, []).append(r)
|
||||
return base, cells
|
||||
|
||||
|
||||
def paired_rate_delta(base_rows, cell_rows):
|
||||
"""同种子配对通关率差(百分点)与配对 SE——跨档同序种子流复用,配对消方差。"""
|
||||
bmap = {int(r["seed"]): 1 if r.get("result") == "victory" else 0 for r in base_rows}
|
||||
diffs = []
|
||||
for r in cell_rows:
|
||||
s = int(r["seed"])
|
||||
if s in bmap:
|
||||
diffs.append((1 if r.get("result") == "victory" else 0) - bmap[s])
|
||||
n = len(diffs)
|
||||
if n == 0:
|
||||
return None
|
||||
m_raw = sum(diffs) / n
|
||||
se = None
|
||||
if n > 1:
|
||||
var = sum((d - m_raw) ** 2 for d in diffs) / (n - 1)
|
||||
se = math.sqrt(var / n) * 100.0
|
||||
return m_raw * 100.0, se, n
|
||||
|
||||
|
||||
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 key in sorted(flats[flat]):
|
||||
d = paired_rate_delta(base.get(key, []), flats[flat][key])
|
||||
if d is None:
|
||||
continue
|
||||
mean_pp, se_pp, n = d
|
||||
per_lvl = mean_pp / lv
|
||||
per_lvl_se = (se_pp / lv) if se_pp is not None else None
|
||||
detail.append({"flat": flat, "key": key, "n": n, "mean_pp": mean_pp,
|
||||
"se_pp": se_pp, "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
|
||||
|
||||
|
||||
ATTR_CN = {"max_hp": "生命上限", "atk": "基础攻击", "armor_class": "护甲等级"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 报告
|
||||
|
||||
def fmt_table(header, rows):
|
||||
@@ -294,24 +480,27 @@ def report(args, rows, meta):
|
||||
A = lines.append
|
||||
A(f"# 数值验证报告({meta['ts']})")
|
||||
A("")
|
||||
A(f"- 采集规模:直入口每格 {args.n} 局 · 基线冒险 {args.na} 局/画像 · 战役 {args.k} 局/画像"
|
||||
A(f"- 采集规模:直入口每格 {args.n} 局(层集 {args.levels} · 注入档 {args.inject or '白板'})"
|
||||
f" · 基线冒险 {args.na} 局/画像 · 战役 {args.k} 局/画像"
|
||||
f" · 种子 {args.seed0} 起逐局独立 · 锻造花费策略 = {args.spend}")
|
||||
A(f"- 切片版本:{meta['commit']} · 表状态:判定带与表值溯源见 tools/balance_verifier.py 文件头")
|
||||
A(f"- 切片版本:{meta['commit']} · 换算率表值(meta.tres 启动解析):"
|
||||
+ " · ".join(f"{ATTR_CN[a]}每级平移{CONVERSION.get(a, 0)}点" for a in OVERRIDE_ATTRS))
|
||||
A("")
|
||||
|
||||
# ① 直入口基线
|
||||
A("## ① 直入口基线通关率(验收带判定)")
|
||||
# ① 直入口基线(只留白板档——注入档读数归 ④ 标定段)
|
||||
A("## ① 直入口基线通关率(验收带判定,白板档)")
|
||||
A("")
|
||||
lvl = analyze_level(rows)
|
||||
lvl = analyze_level(rows, baseline_only=True)
|
||||
tbl = []
|
||||
for (policy, li), a in sorted(lvl.items()):
|
||||
for key, a in sorted(lvl.items()):
|
||||
policy, li = key[0], key[1]
|
||||
rate = 100.0 * a["wins"] / max(1, a["n"])
|
||||
ci = 1.96 * math.sqrt((rate / 100) * (1 - rate / 100) / max(1, a["n"])) * 100
|
||||
band = LEVEL_BANDS.get((policy, li))
|
||||
verdict, _ = judge_band(rate, band)
|
||||
tbl.append([f"{LEVEL_CN[li]}", "普通档" if policy == "normal" else "完美档",
|
||||
f"{a['wins']}/{a['n']}", f"{rate:.1f}% ±{ci:.1f}",
|
||||
f"[{band[0]},{band[1]}]" if band else "未定", verdict])
|
||||
f"[{band[0]:g},{band[1]:g}]" if band else "未定", verdict])
|
||||
A(fmt_table(["层", "画像", "通关/局数", "通关率±95%CI", "验收带", "判定"], tbl))
|
||||
A("")
|
||||
|
||||
@@ -321,6 +510,8 @@ def report(args, rows, meta):
|
||||
adv = analyze_adventure(rows)
|
||||
for policy in ("normal", "perfect"):
|
||||
d = adv[policy]
|
||||
if d["n"] == 0:
|
||||
continue
|
||||
cn = "普通档" if policy == "normal" else "完美档"
|
||||
A(f"### {cn}({d['n']} 局)")
|
||||
A("")
|
||||
@@ -333,65 +524,125 @@ 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)},表值平移 {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 ""))
|
||||
A("")
|
||||
tbl = []
|
||||
series = d.get("series", {})
|
||||
runs_by_no = {r["run_no"]: r for r in d["runs"]}
|
||||
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:
|
||||
any_detail = False
|
||||
for attr in OVERRIDE_ATTRS:
|
||||
e = est.get(attr)
|
||||
if not e or not e["detail"]:
|
||||
continue
|
||||
any_detail = True
|
||||
per_flat = max(1, FORGE_FLAT.get(attr, 1))
|
||||
A(f"### {ATTR_CN[attr]}(每级 flat +{per_flat})")
|
||||
A("")
|
||||
tbl = []
|
||||
for d in e["detail"]:
|
||||
policy, li = d["key"]
|
||||
tbl.append([
|
||||
f"+{d['flat']}({d['flat'] // per_flat} 级)",
|
||||
"普通档" if policy == "normal" else "完美档", f"{LEVEL_CN[li]}",
|
||||
str(d["n"]),
|
||||
f"{d['mean_pp']:+.1f}" + (f" ±{d['se_pp']:.1f}" if d["se_pp"] is not None else ""),
|
||||
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("")
|
||||
if any_detail:
|
||||
A("### 三行汇总与建议投值")
|
||||
A("")
|
||||
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(e["table_val"] if e else 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:
|
||||
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("")
|
||||
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("- 合并口径:逆方差加权跨(档位×画像×层);带平移为线性单值近似——近带顶/带底的读数受天花板压缩,判读看 ④ 明细表")
|
||||
A("")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def console_verdict(args, rows):
|
||||
"""控制台判定结论(先读数后细节)。"""
|
||||
lvl = analyze_level(rows)
|
||||
lvl = analyze_level(rows, baseline_only=True)
|
||||
print("\n判定结论(detail 见报告文件)")
|
||||
for (policy, li), a in sorted(lvl.items()):
|
||||
for key, a in sorted(lvl.items()):
|
||||
policy, li = key[0], key[1]
|
||||
rate = 100.0 * a["wins"] / max(1, a["n"])
|
||||
band = LEVEL_BANDS.get((policy, li))
|
||||
verdict, ok = judge_band(rate, band)
|
||||
@@ -401,20 +652,33 @@ def console_verdict(args, rows):
|
||||
adv = analyze_adventure(rows)
|
||||
for policy in ("normal", "perfect"):
|
||||
d = adv[policy]
|
||||
if d["n"] == 0:
|
||||
continue
|
||||
cn = "普通档" if policy == "normal" else "完美档"
|
||||
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()
|
||||
|
||||
|
||||
@@ -430,14 +694,19 @@ def main():
|
||||
ap.add_argument("--k", type=int, default=60, help="战役局数/画像")
|
||||
ap.add_argument("--seed0", type=int, default=8000)
|
||||
ap.add_argument("--only", default="level,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("--levels", default="1,2,3", help="直入口层集(标定批常用 1,2——第三层带未定)")
|
||||
ap.add_argument("--timeout", type=int, default=5400)
|
||||
ap.add_argument("--quick", action="store_true", help="小规模快跑(12/8/10)")
|
||||
ap.add_argument("--from-raw", default=None, help="跳过采集,对既有 raw.jsonl 重新判定出报告")
|
||||
args = ap.parse_args()
|
||||
if args.quick:
|
||||
args.n, args.na, args.k = 12, 8, 10
|
||||
args.est_min = (3 * 2 * args.n * 1.5 + 2 * args.na * 3.5 + 2 * args.k * 3.5) / 60
|
||||
n_levels = len([s for s in args.levels.split(",") if s.strip()])
|
||||
args.est_min = (n_levels * 2 * args.n * 1.5 + 2 * args.na * 3.5 + 2 * args.k * 3.5) / 60
|
||||
|
||||
load_meta_table() # 表值锚配置推导化——解析失败即 FAIL,零硬编码兜底
|
||||
|
||||
ts = time.strftime("%Y%m%d-%H%M%S")
|
||||
if args.from_raw:
|
||||
|
||||
Reference in new issue
Block a user