merge: 测量面去单关直入口——难度读数改冒险条件口径(用户指令 2026-09-23)
This commit is contained in:
commit
61874f77ad
14 files changed
+119
-331
No files matched your search
@@ -20,8 +20,8 @@
|
||||
|
||||
- 冒烟(全量 headless 断言):`python tools/run_smoke.py`(包装器三闸:退出码+摘要失败数+SCRIPT ERROR 扫描——脚本错误不计冒烟失败数且静默中断所在函数剩余断言,2026-09-19 台账 84 立闸)。底层命令 = `…console.exe --headless --path . --script res://smoke/smoke_all.gd`
|
||||
- 场景级探针(呈现层回归):`…console.exe --headless --path . res://smoke/ui_probe.tscn`;另有 `ui_tune_probe.tscn`(数值调整板)、`ui_click_probe.tscn`(合成鼠标事件行为探针)
|
||||
- 调参期批量工具:`--script res://smoke/level_monte_carlo.gd`(关卡期望扫描)· `--script res://smoke/reward_expectation.gd`(奖励获取期望)
|
||||
- 数值验证工具:`python tools/balance_verifier.py`(直入口基线带判定+零强化止步节点归因+营地循环爬升战役——采集端 `--script res://smoke/verifier_batch.gd`,报告落 `production/qa/`;元进度换算率标定与曲线调参的自动化评判面,2026-09-22 数值验证工具批)
|
||||
- 调参期批量工具:`--script res://smoke/adventure_monte_carlo.gd`(全冒险期望扫描)· `--script res://smoke/reward_expectation.gd`(奖励获取期望)——单关直入口口径已退役(2026-09-23 用户拍板),通关率/难度类测量一律走全冒险(原 level_monte_carlo/level_debug 已删)
|
||||
- 数值验证工具:`python tools/balance_verifier.py`(零强化冒险逐关条件通关率+止步节点归因+营地循环爬升战役——采集端 `--script res://smoke/verifier_batch.gd`,报告落 `production/qa/`;元进度换算率标定与曲线调参的自动化评判面,2026-09-22 数值验证工具批;直入口带判定随口径退役暂停,带重锚归曲线调参批)
|
||||
- 交付前验收 = smoke + 全部场景探针全绿(测试细则见 `docs/code-conventions.md`)
|
||||
|
||||
## Project Layout
|
||||
|
||||
@@ -95,4 +95,4 @@
|
||||
- **Boss 战必须单独出列**(硬化行为变化——旧 Boss 承伤 84.8% 等读数全部作废)。
|
||||
- 普通场承伤带、通关率基线全量重测(削弱续时 + 乘区接线 + 甲效量纲换装 + 充能退役 多变量叠加,禁与任何旧读数混算)。
|
||||
- 旧读数作废清单:池触底免伤轮相关读数、硬化翻倍期全部战斗读数、乏力死效果期格面调研读数、充能期(三分量输出)全部战斗读数。
|
||||
- 工具就位:`analyze_playtest.py`(反事实分析,吃新日志)、`level_monte_carlo`(MC 基线重跑)、JSONL 日志含池值与快照。
|
||||
- 工具就位:`analyze_playtest.py`(反事实分析,吃新日志)、MC 基线重跑走 `balance_verifier.py` 冒险段(原 level_monte_carlo 随单关直入口口径退役,2026-09-23)、JSONL 日志含池值与快照。
|
||||
@@ -112,7 +112,7 @@ MVP 行值并不逐式现算,而是按此式量级直填经切片验证过的
|
||||
|
||||
| 带 | 定稿口径 | 含义 |
|
||||
|---|---|---|
|
||||
| 基线通关率带 | 基线档(无元进度加成)通关率:**第 1 关 ∈ [35%, 55%]、第 2 关 ∈ [25%, 45%]**(2026-09-20 用户拍板分带——旧带 [10%, 25%] 随普通画像换装基础定式后失锚作废:旧带锚定的是随机点击画像,基础定式+引导后玩家的真人体验换算(模拟 47%/34% 对应真人约六成/五成)反推落此;更早的 [30%, 50%] 系随机画像口径亦作废) | 首关吃紧但多数玩家数局内能过;第二关明显更难;与完美档带 [35%, 65%] 错开一档留出元进度爬升空间 |
|
||||
| 基线通关率带 | ⚠ 测量口径退役待重锚(2026-09-23 用户拍板单关直入口口径退役):原带 **第 1 关 ∈ [35%, 55%]、第 2 关 ∈ [25%, 45%]** 锚定直入口单关读数(满血零上下文进关)——与真实冒险的条件通关率脱节(实证:直入口 L2 普通 5% vs 营地检查点 L2 条件 90% 同期并存),逐关读数改冒险条件口径派生(balance_verifier 冒险段),旧带值不可比;新带重锚归曲线调参批,重锚前只测不判(原带推导史:2026-09-20 分带批,更早 [10%, 25%]/[30%, 50%] 系随机画像口径先后作废) | 首关吃紧但多数玩家数局内能过;第二关明显更难;与完美档带错开一档留出元进度爬升空间——重锚时按此设计意图在新口径上取值 |
|
||||
| 通关余量带 | 通关者通关时剩余生命的 p90 / p10 按现生命轴上限百分比表达(压缩前绝对血量带已作废;界值重标随验收批) | **通关余量** = 通关瞬间还剩多少血。余量太肥说明赢得太从容,压生存侧;太瘦说明"剩一两滴血惨胜"成常态,抬治疗供给或恢复点收益 |
|
||||
| 第一关形态带 | 教学位绿档 + 中段黄档 + Boss 黄档不红 | **形态带**管一关内各战三档判定的排布形状:第一战必须稳过,中段有输的可能,Boss 战紧但不许是配置事故 |
|
||||
|
||||
@@ -281,7 +281,7 @@ Boss 战的棋盘用本关最大板形(板形行值读棋盘参数表),板
|
||||
- 零红档:全部行集经区间扫描,不存在"最好情况也打不过"的战斗组合。
|
||||
- 教学位绿档:第一关第一战与各新敌种首遇位的区间判定为绿档。
|
||||
- 形态带成立:第一关形态 = 教学位绿 + 中段黄 + Boss 黄不红。
|
||||
- 通关率带:基线档通关率第 1 关 ∈ [35%, 55%]、第 2 关 ∈ [25%, 45%](2026-09-20 用户拍板分带,旧带 [10%, 25%] 作废——随机画像口径失锚)——重投基线读数 L1 47%/L2 34% 落带(2026-09-20,N=300/画像,production/qa/mc-reinvest-baseline-2026-09-20.md)。
|
||||
- 通关率带:⚠ 口径退役待重锚(2026-09-23)——原带第 1 关 ∈ [35%, 55%]、第 2 关 ∈ [25%, 45%](2026-09-20 分带批)锚定直入口单关读数,该口径随单关试玩入口一并退役;逐关读数改冒险条件口径(balance_verifier 报告①),新带值重锚归曲线调参批,重锚前只测不判(旧重投读数 L1 47%/L2 34% 系直入口口径,production/qa/mc-reinvest-baseline-2026-09-20.md 留档不作判据)。
|
||||
- 通关余量带:通关者剩余生命 p90 / p10 按现生命轴上限百分比表达,界值重标随验收批。
|
||||
- 恒等式闭合:关级通关恒等式逐项对账闭合,残差 <5%(provisional 阈值);奖励输出占比 ∈ [15%, 40%]。
|
||||
- Boss 锚在案:每个 Boss 行的血量锚比率记录在案且落在现行带内;越带须登记,不静默变更。
|
||||
@@ -315,3 +315,4 @@ Boss 战的棋盘用本关最大板形(板形行值读棋盘参数表),板
|
||||
| v0.16 | 2026-09-20 | **基线通关率带分带批 + MC 重投基线(用户拍板方案 A;台账 105〔登记时撞并行会话 103 号,顺延让号〕)**:① 基线通关率带由单一 [10%, 25%] 改为分带——第 1 关 [35%, 55%] / 第 2 关 [25%, 45%]:旧带锚定随机点击画像,普通画像换装基础定式后失锚;推导链 = 真人体验换算(模拟 47%/34% 对应真人新手约六成/五成)+ 与完美档带 [35,65] 错开一档保留元进度爬升空间;② MC 重投基线(N=300/画像/关 · unique · seed0=7000):完美档 55.7%/39.7% 落 [35,65]、普通档 46.7%/34.3% 落新带——双画像双关全落带,加难紧迫性解除;③ 残留越带唯一项 = L2 完美 Boss 承伤 51.6%(微越 1.6 点,归调参批);④ 报告 production/qa/mc-reinvest-baseline-2026-09-20.md;工具 level_monte_carlo.gd 增 baseline 参数 + F5 脚注随带同步 |
|
||||
| v0.17 | 2026-09-20 | **第三关内容批(灰矮人矿城——与切片代码 / registry / 03-内容规模 / 棋盘生成 / 地形变异 同批,存量行值零改动)**:调参表新增第三关图版注记——敌种生态(灰矮人 duergar 单体重甲中坚 + 钩爪恐魔 hook_horror 群行复用撕咬 + Boss 灰矮人石君王 duergar_king 专属池独占妨害技大地脉动 = 乏力 sapped 第二妨害词汇首发)、首遇位节点 0/1 固定数量不加骰、克制面重轻错开(重甲首次登台,教学梯度轻→魔→重在 L3 补完)、Boss 血量锚 75 = Σ91 × r_boss≈0.82、棋盘 13×13 / Boss 14×14 密度 0.25 维持、地形落石 3 点 + 矿尘、armor_class 恒 0(正式投值仍归在案条目全表同批);期望值带节补 L3 口径——行值 provisional-pending-growth,零成长基线红档属预期、冒烟首遇绿档按成长代理档(atk+200 / hp+100)口径,正式通关率带与首遇绿档口径随 L3-A 玩家成长批(GP + 商店房)换装,归曲线调参批统一收带 |
|
||||
| v0.18 | 2026-09-20 | **GP+商店房批(用户拍板"骨架先行、数值整体后置"——与 关卡节点地图 v1.1 / 冒险结构 v0.5 / 战斗结算 v0.23 / 地图战斗资源 v0.15 / registry / 切片同批,行值零改动)**:① 新增「商店与 GP 经济杠杆」节——GP 供给 × 商品定价 = 第三族玩家侧杠杆(掉落均值±方差 / 关级掉落加值 / 关级价格加成 / 首价与步长,全部读 shop.tres provisional-pending-曲线调参批),定价锚定口径 = 正常运气与运气极好可购档数的数学期望(期望账进商店表批注);② 分层验收 defer 成文——shop=off/on 双格 MC、shop=on 带平移、换算率登记全部随曲线调参批执行(与「基线通关率越带」「L2 Boss 承伤微越带」「大奖励数值联动」同窗),执行前 shop=on 只登记不判读;③ L3 首遇绿档成长代理档口径不变;④ 调参联动表 +GP 供给↔商品定价行、调参表 +shop.tres 行、验收标准 +杠杆在案条目 |
|
||||
| v0.19 | 2026-09-23 | **基线通关率带测量口径退役(用户拍板单关直入口口径退役——与单关试玩入口退役同批)**:原带(第 1 关 [35,55] / 第 2 关 [25,45],v0.16 分带批)锚定直入口单关读数——满血零上下文进关,与真实冒险的条件通关率脱节(实证:直入口 L2 普通 5% 与营地检查点 L2 条件 90% 同期并存,曾误导曲线调参)。逐关读数改冒险条件口径(balance_verifier 冒险段派生:通关数/到达数);新带值重锚归曲线调参批,重锚前只测不判;调参表「基线通关率带」行与验收标准通关率带条同步改口径退役注记。行值零改动 |
|
||||
@@ -528,9 +528,9 @@ entities:
|
||||
owner: design/gdd/systems/冒险循环/难度曲线与Boss.md
|
||||
attributes:
|
||||
kind: 调参目标带(F5)+ 承伤估算(F4)+ 供给平衡式(F6)——设计期工具式,运行时零新增公式(难度曲线与Boss 定位声明)
|
||||
clear_rate_target: L1 基线 **10-25%**(2026-09-17 数值纪律收口批用户拍板——旧 30-50% 带作废,乱点玩家画像不真实、引导后玩家逼近完美策略;压缩批后旧扫描读数 60% 系旧带旧值链口径、已失效,重测随验收批另批登记)/ L2 20-40% / L3+ 10-25%(中高加成带见 GDD F5 表——区间位移机制承载:atk 翻倍 ≈ K 减半 → 绿档;2026-09-18 起永久强化经换算率表按带平移接入,见 meta_progression_conversion_table)
|
||||
clear_rate_target: ⚠ 口径退役待重锚(2026-09-23 用户拍板单关直入口口径退役——逐关读数改冒险条件口径派生〔balance_verifier 冒险段〕,带值重锚归曲线调参批,重锚前只测不判;带史 = 2026-09-20 分带批 L1 [35,55]/L2 [25,45] 锚定直入口单关读数〔已随口径退役不可比〕,更早 L1 10-25/L2 20-40 系乱点画像口径亦作废;区间位移机制承载:atk 翻倍 ≈ K 减半 → 绿档;2026-09-18 起永久强化经换算率表按带平移接入,见 meta_progression_conversion_table)
|
||||
supply_balance_band: "**关级供给带 [1.1,2.0] 退役(2026-09-14——其验算 0.54 与实测 60% 通关矛盾)**——由生存链区间模型替代:每战 S/K 区间三档判定(绿 S_min>K_max / 黄 E[S]≥E[K]+1 / 红 S_max≤K_min 禁止入表)+ 关级恒等式(atk×ΣR_survive + Σ奖励输出 ≈ Σ敌hp期望×(1+缓冲),actuals 逐项审计闭合)+ 输出占比带 [15%,40%] + 通关余量带(p90≤13 & p10≥3——'通关只剩一点血'量化)"
|
||||
tool_carrier: level_monte_carlo.gd / reward_expectation.gd(切片原型→难度曲线与Boss 期正式批量工具,棋盘生成 F4 排期倒挂解除载体)
|
||||
tool_carrier: balance_verifier.py 冒险段(逐关条件通关率/止步归因/营地循环——2026-09-23 起难度读数主载体)/ adventure_monte_carlo.gd / reward_expectation.gd(原 level_monte_carlo.gd 随单关直入口口径退役删除 2026-09-23;棋盘生成 F4 排期倒挂解除载体)
|
||||
calibration_gap: 真人修正系数待 playtest 标定(OQ3——**方向已由试玩批次 1 反向确认:真人显著强于模拟者**〔v2 真人 100% vs 模拟 80%〕;旧"真人预期低于模拟"方向声明由本条替代;带按模拟口径定义,真人系数批次 2 定量)
|
||||
referenced_by: [地图战斗资源, 战斗结算, 冒险结构, 元进度]
|
||||
|
||||
@@ -555,7 +555,7 @@ entities:
|
||||
nodes: 节点图结构——节点数组 + 连线(node_kind ∈ {battle, reward, rest, boss, shop},切片字段名 type;+shop 随 2026-09-20 GP+商店房批——商店房兑付 = 打开商店覆盖层、离开后确认回选路,不携带战斗行〔零敌人、零棋盘配置行〕;每节点领关卡内唯一**节点编号**,battle/boss 数据键 node_depth 沿用〔v1.0 语义升级:深度 → 节点编号〕;连线 = 出边节点编号表,只朝前进方向、无回头路);单链特例 = 隐式后继(数组序即链,切片现行形态零漂移)
|
||||
rowset_rule: 图合法性校验(2026-09-17 关卡节点地图 v1.0)——节点编号关卡内唯一、入口唯一、Boss 恒唯一终点(编号最大,Dmax 语义平移)、连线无环、所有节点沿连线可达 Boss,违规加载期致命;实现批(2026-09-17 分支节点图批):校验落 LevelTable.validate(单链形态经同一套算法——编号解析单链 = 数组下标 / 图 = node_depth),L1 单链与 L2 图双行集加载全绿断言;商店房编排(每关恰 1、Boss 前一位必经位)由冒烟断言承载、非加载校验——图合法性六检零新增(GP+商店房批 2026-09-20,关卡节点地图 v1.1)
|
||||
cross_table_validation: battle/boss 节点 (L,D) 在 board_gen/enemy 两表存在精确行(数据配置 跨表校验⑧复用 + 冒险结构 AC-7 冒烟断言——序列关数 ⊆ 内容覆盖,越界关卡 = 内容生产缺口)
|
||||
mvp_content: L1 全行(切片转正——7 节点链〔单链特例,合法图形态〕、rest_heal_pct=30);L2 全行已落地(2026-09-17 分支节点图批——首张分支图:9 节点〔入口 0 → 中间三层 2/3/2 → Boss 8〕、真实岔路 4 处、奖励点+恢复点各 1、战斗节点 6 + Boss;行值 provisional 归 难度曲线与Boss v0.7;首页直入口试玩模式保留);**L3 全行已落地(2026-09-20 第三关内容批——「第三层 · 灰矮人矿城」:九节点图复用 L2 同模板〔6 战 + Boss + 奖励 + 恢复、真实岔路 4 处〕,行值全 provisional 归 难度曲线与Boss;测试直入口保留)**;2026-09-20 GP+商店房批 rewiring:L2/L3 图版 Boss 重编号 8→9、各新增 (8, shop, next[9]) 商店房节点(Boss 前一位必经位),L1 零改动,图合法性六检零新增——必经位由冒烟断言承载;**2026-09-21 冒险结构实现批:三关正式序列生效**(首页「开始冒险」= L1→L2→L3 连探,AdventureRunner 编排——大奖励/生命账本/GP 冒险账本/主种子归冒险结构层,冒烟断言在案)**
|
||||
mvp_content: L1 全行(切片转正——7 节点链〔单链特例,合法图形态〕、rest_heal_pct=30);L2 全行已落地(2026-09-17 分支节点图批——首张分支图:9 节点〔入口 0 → 中间三层 2/3/2 → Boss 8〕、真实岔路 4 处、奖励点+恢复点各 1、战斗节点 6 + Boss;行值 provisional 归 难度曲线与Boss v0.7;首页直入口已退役〔2026-09-23 用户拍板〕);**L3 全行已落地(2026-09-20 第三关内容批——「第三层 · 灰矮人矿城」:九节点图复用 L2 同模板〔6 战 + Boss + 奖励 + 恢复、真实岔路 4 处〕,行值全 provisional 归 难度曲线与Boss;测试直入口已退役〔2026-09-23〕)**;2026-09-20 GP+商店房批 rewiring:L2/L3 图版 Boss 重编号 8→9、各新增 (8, shop, next[9]) 商店房节点(Boss 前一位必经位),L1 零改动,图合法性六检零新增——必经位由冒烟断言承载;**2026-09-21 冒险结构实现批:三关正式序列生效**(首页「开始冒险」= L1→L2→L3 连探,AdventureRunner 编排——大奖励/生命账本/GP 冒险账本/主种子归冒险结构层,冒烟断言在案)**
|
||||
referenced_by: [冒险结构, 关卡节点地图, 难度曲线与Boss, 棋盘生成, 数据配置]
|
||||
|
||||
- name: adventure_table
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
("可选类 3.32"不加"每板",会被读成每格期望 >1)。
|
||||
|
||||
### 5.2 调参先测量,改后复测
|
||||
改任何玩法数值前,先用扫描工具产出基线(`level_monte_carlo.gd` / `reward_expectation.gd`);
|
||||
改任何玩法数值前,先用扫描工具产出基线(通关率/难度类读数走 `balance_verifier.py` 冒险段——单关直入口口径已退役,机制类读数走 `reward_expectation.gd` 等);
|
||||
改完必须复测回填数据。禁止拍脑袋调参。
|
||||
|
||||
### 5.3 数值设计必须跨尺度推演
|
||||
|
||||
@@ -33,23 +33,25 @@ Godot_v4.7.2-stable_win64_console.exe --headless --path . res://smoke/ui_probe.t
|
||||
Godot_v4.7.2-stable_win64_console.exe --headless --path . res://smoke/ui_tune_probe.tscn
|
||||
```
|
||||
|
||||
关卡期望扫描(C3 调参期关卡侧批量工具原型——atk%×hp% 网格,每格输出通关率/承伤期望/败北节点):
|
||||
全冒险期望扫描(零加成三关连探批量——通关率/止步分布/败退节点;调参期难度读数一律走全冒险口径):
|
||||
|
||||
```
|
||||
Godot_v4.7.2-stable_win64_console.exe --headless --path . --script res://smoke/level_monte_carlo.gd -- [N] [seed0]
|
||||
Godot_v4.7.2-stable_win64_console.exe --headless --path . --script res://smoke/adventure_monte_carlo.gd -- [N] [seed0]
|
||||
```
|
||||
|
||||
(原关卡期望扫描 level_monte_carlo.gd 走单关直入口口径——满血零上下文进关与真实冒险脱节,随口径退役删除,2026-09-23 用户拍板。)
|
||||
|
||||
可选类供给跨种子扫描(playtest-2026-09-12 Top1 复核——每局治疗包/小刀/精工短刀获取数、构成占比对 9:1 配比、"≥3 治疗包"局数分布;单种子坏抽与系统性偏置由此分割):
|
||||
|
||||
```
|
||||
Godot_v4.7.2-stable_win64_console.exe --headless --path . --script res://smoke/supply_scan.gd -- [N] [seed0]
|
||||
```
|
||||
|
||||
**日志合规(2026-09-12 用户要求)**:任何模拟运行与真人试玩均落全量 JSONL(user://logs/)——逐操作(op/cell/accepted/round)、逐结算明细行、轮次状态快照、battle_end、level_end。模拟(level_monte_carlo / 调整板批量模拟 / supply_scan)一文件多局,run 行携带种子/档位切分局次。
|
||||
**日志合规(2026-09-12 用户要求)**:任何模拟运行与真人试玩均落全量 JSONL(user://logs/)——逐操作(op/cell/accepted/round)、逐结算明细行、轮次状态快照、battle_end、level_end。模拟(verifier/adventure_monte_carlo / 调整板批量模拟 / supply_scan)一文件多局,run 行携带种子/档位切分局次。
|
||||
|
||||
**试玩画像口径(2026-09-19 用户拍板固化——全项目试玩/扫描/面板模拟一律只用以下两档,随机策略口径退役)**:`policy="normal"` = **普通画像·基础定式**(单数字满足规则——满足即标/满足即开,无处可推盲挖边界格、不刷新;教学后正常玩家的下限近似);`policy="perfect"` = **完美画像**(single+subset 全推理零盲挖/卡死刷新——技能上界仪)。实现单一出口 = `smoke/auto_player.gd`(novice_action / perfect_action)经 `smoke/level_autoplay.gd`,调整板批量模拟与 level_monte_carlo 自动同口径;战斗层冒烟夹具与调试转储的 `choose_dig` 直调是机制测试夹具、不承载画像语义。分级依据与新旧读数(乱点 0%/5% → 基础定式 65%/90%)见 auto_player.gd 普通策略档节与台账 77。
|
||||
**试玩画像口径(2026-09-19 用户拍板固化——全项目试玩/扫描/面板模拟一律只用以下两档,随机策略口径退役)**:`policy="normal"` = **普通画像·基础定式**(单数字满足规则——满足即标/满足即开,无处可推盲挖边界格、不刷新;教学后正常玩家的下限近似);`policy="perfect"` = **完美画像**(single+subset 全推理零盲挖/卡死刷新——技能上界仪)。实现单一出口 = `smoke/auto_player.gd`(novice_action / perfect_action)经 `smoke/level_autoplay.gd`,调整板批量模拟与各扫描工具自动同口径;战斗层冒烟夹具与调试转储的 `choose_dig` 直调是机制测试夹具、不承载画像语义。分级依据与新旧读数(乱点 0%/5% → 基础定式 65%/90%)见 auto_player.gd 普通策略档节与台账 77。
|
||||
|
||||
单关诊断:`--script res://smoke/level_debug.gd -- [seed] [atk%] [hp%]`。
|
||||
单关诊断工具(level_debug.gd)随单关直入口口径退役(2026-09-23)——败退归因走 balance_verifier 冒险段(层/节点/敌人人读归因)。
|
||||
战斗级工具(v1 遗产):`dump_battle.gd`(结算流水)/ `monte_carlo.gd`(战斗期望)/ `experiment_density.gd`(密度调参)。
|
||||
|
||||
## 范围(v1 范围 + v2 追加,用户 2026-09-10/11 确认)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# 棋盘板形统计探针(清图支配性对账重跑批取数工具——判式"实测板形"口径的数据源):
|
||||
# 对 board_gen.tres 每行 (L, D) 生成 N 张板,统计幸存雷数 |M| 与安全格数 |S|
|
||||
# 的均值 / 最小 / 最大("先布后删"后的落地值——名义值 density×(W×H−9) 不含删除段)。
|
||||
# 只读诊断,零游戏代码改动;装载面与 level_monte_carlo 同源(config_db.load_tables)。
|
||||
# 只读诊断,零游戏代码改动;装载面与冒险扫描工具同源(config_db.load_tables)。
|
||||
# 运行:Godot_console.exe --headless --path . --script res://smoke/board_stat_probe.gd -- [N] [seed0]
|
||||
extends SceneTree
|
||||
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
# Date: 2026-09-11(2026-09-12 增完整遥测接线——用户要求:任何模拟运行与真人试玩同日志合规:
|
||||
# 操作记录 / 轮次状态 / 战斗结束 / 通关结束 全量 JSONL。logger=null 时自建会话(每局一文件),
|
||||
# 批量调用方传共享 logger(log_run 局次标记切分)。
|
||||
# 关卡自动通关策略(理性玩家近似)——呈现层数值调整板的批量模拟与 headless 期望扫描(level_monte_carlo)
|
||||
# 关卡自动通关策略(理性玩家近似)——呈现层数值调整板的批量模拟与 headless 扫描
|
||||
# 共用同一实现,保证"面板模拟读数"与"扫描表读数"同口径。策略与 smoke_all 战斗自动玩家同族:
|
||||
# 挖最近可挖格 / 无可挖停手 / 奖励恢复点立即确认。
|
||||
# 2026-09-23 单关直入口口径退役(用户拍板):run_level 降格为机制夹具/单战锚点专用
|
||||
# (smoke_all 机制断言、rhythm/defense/damage 单战扫描)——通关率/难度类测量禁用本入口
|
||||
# (满血零上下文进关,与真实冒险条件通关率脱节),一律走 run_adventure。
|
||||
# 2026-09-15 格面资源批:道具窗口选择与可选类获取计数随可选类道具删除下线。
|
||||
# 2026-09-14 增完美策略档(policy="perfect",默认 "normal" 零行为变更——既有调用方签名兼容):
|
||||
# 零盲挖/全标旗/卡死刷新——上界测量仪,求解器与动作语义见 auto_player.gd 完美策略档节。
|
||||
@@ -14,8 +17,8 @@
|
||||
# 2026-09-21 冒险结构实现批:run_adventure 三关连探驱动(AdventureRunner——主种子恰一次、
|
||||
# 大奖励立即确认 continue_to_next_level、adventure_finished 置 result/summary;外层每关重取
|
||||
# runner 实例经 _drive_level 复用单关驱动)。run_level 驱动段抽成 _drive_level——签名与
|
||||
# 返回结构零变化,行为逐位不变(MC 同种子对照闸承载)。
|
||||
# 运行前提:db 已 load_tables;每个 run_level 全新 RngService(同种子 ⇒ 全程逐步可回放)。
|
||||
# 返回结构零变化,行为逐位不变。
|
||||
# 运行前提:db 已 load_tables;run_level/run_adventure 均须全新 RngService(同种子 ⇒ 全程逐步可回放)。
|
||||
# 实现注:信号处理体全部为静态方法 + bind 传状态——GDScript 多行 lambda 内嵌 match/控制流的解析不可靠。
|
||||
extends RefCounted
|
||||
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Date: 2026-09-11
|
||||
# 单关卡诊断:跑一遍完整关卡并打印进度与统计(定位挂起/数值问题的临时工具)。
|
||||
# 运行:Godot_console.exe --headless --path . --script res://smoke/level_debug.gd -- [seed] [atk%] [hp%]
|
||||
extends SceneTree
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var args := OS.get_cmdline_user_args()
|
||||
var seed_val := 4200
|
||||
var atk_pct := 0
|
||||
var hp_pct := 0
|
||||
if args.size() >= 1 and args[0].is_valid_int():
|
||||
seed_val = int(args[0])
|
||||
if args.size() >= 2 and args[1].is_valid_int():
|
||||
atk_pct = int(args[1])
|
||||
if args.size() >= 3 and args[2].is_valid_int():
|
||||
hp_pct = int(args[2])
|
||||
var db = load("res://core/config_db.gd").new()
|
||||
if not db.load_tables("res://data/tables", false):
|
||||
print("表加载失败")
|
||||
for d in db.diagnostics():
|
||||
if d.severity == "error":
|
||||
print(" error: [%s] %s" % [d.category, d.message])
|
||||
quit(1)
|
||||
return
|
||||
var autoplay = load("res://smoke/level_autoplay.gd")
|
||||
var t0 := Time.get_ticks_msec()
|
||||
print("开始:seed=%d atk+%d%% hp+%d%%" % [seed_val, atk_pct, hp_pct])
|
||||
var st = autoplay.run_level(db, seed_val, 1, atk_pct, hp_pct)
|
||||
print("结果: %s · 用时 %.2f 秒" % [st.result, float(Time.get_ticks_msec() - t0) / 1000.0])
|
||||
print(" 战斗节点=%d 轮数=%d 整关承伤=%d 剩余HP=%d 进BossHP=%d 踩雷=%d" % [st.battles, st.rounds, st.hero_dmg, st.hp_left, st.hp_at_boss, st.mine_hits])
|
||||
print(" 败北节点=%d(%s) 恢复点回复=%d(前HP %d)" % [st.defeat_node_index, st.defeat_node_type, st.rest_healed, st.hp_before_rest])
|
||||
quit(0)
|
||||
@@ -1 +0,0 @@
|
||||
uid://bflo5h8xysqsl
|
||||
@@ -1,169 +0,0 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Date: 2026-09-11
|
||||
# 关卡期望扫描(难度曲线与Boss 调参期关卡侧批量工具的原型——对应 棋盘生成 F4 / 战斗侧 monte_carlo.gd):
|
||||
# 扫描 atk% × hp% 网格(模拟天赋树数值提升档位),输出每档"通关率 / 整关承伤期望 / 败北节点分布 /
|
||||
# 通关剩余 HP 期望 / 进 Boss HP 期望 / 平均轮数"——把"多少数值能通关、通关要掉多少血"变成可测量数据。
|
||||
# 运行:Godot_console.exe --headless --path . --script res://smoke/level_monte_carlo.gd -- [N] [seed0] [normal|perfect|both] [level_index] [shared|unique]
|
||||
# 种子模式(2026-09-19 掷点测量修复批 A 案):shared(默认,旧行为)= 同批次全部格子×策略共用种子
|
||||
# seed0+i——同种子历史坐标可比,但同一 (种子, scope) 被反复消费,聚合掷点统计被头部窗口彩票放大
|
||||
# (验收重测批实证);unique = 逐局独立种子(seed0 + 全局递增序号),每个掷点恰消费一次 → 聚合
|
||||
# 掷点统计回归标准二项分布(±2σ gate 口径重新有效)。验收掷点面用 unique;同种子 A/B 对照用 shared。
|
||||
extends SceneTree
|
||||
|
||||
|
||||
var _run_cursor := 0 # unique 模式全局局序号(seed = seed0 + cursor)
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var args := OS.get_cmdline_user_args()
|
||||
var n := 30
|
||||
if args.size() >= 1 and args[0].is_valid_int():
|
||||
n = maxi(5, int(args[0]))
|
||||
var seed0 := 4200
|
||||
if args.size() >= 2 and args[1].is_valid_int():
|
||||
seed0 = int(args[1])
|
||||
# 关卡轴(分支节点图批 2026-09-17:L2 幽暗地域图版扫描——第 4 参 level_index,默认 1 零行为变更)
|
||||
var level_arg := 1
|
||||
if args.size() >= 4 and args[3].is_valid_int():
|
||||
level_arg = maxi(1, int(args[3]))
|
||||
var db = load("res://core/config_db.gd").new()
|
||||
if not db.load_tables("res://data/tables", false):
|
||||
print("表加载失败")
|
||||
quit(1)
|
||||
return
|
||||
# 共享日志会话(2026-09-12 用户要求:模拟运行与真人试玩同日志合规——操作/轮次/战斗/通关全量 JSONL,
|
||||
# 一文件多局,run 行携带种子/档位切分局次)
|
||||
var logger = load("res://presentation/playtest_logger.gd").new()
|
||||
logger.ap_per_round = int(db.action_economy.ap_per_round)
|
||||
var log_path: String = logger.start_session(seed0)
|
||||
print("模拟日志: %s" % log_path)
|
||||
var autoplay = load("res://smoke/level_autoplay.gd")
|
||||
var t0 := Time.get_ticks_msec()
|
||||
# 策略轴(2026-09-14 OQ7 完美策略档——技能轴上界测量):normal = 原理性玩家近似;
|
||||
# perfect = 零盲挖/全标旗/卡死刷新(auto_player.gd 完美策略档节);both = 双档对照。
|
||||
var policy_arg := "normal"
|
||||
if args.size() >= 3 and args[2] in ["normal", "perfect", "both"]:
|
||||
policy_arg = args[2]
|
||||
var policies: Array = ["normal"]
|
||||
if policy_arg == "perfect":
|
||||
policies = ["perfect"]
|
||||
elif policy_arg == "both":
|
||||
policies = ["normal", "perfect"]
|
||||
# 种子模式(掷点测量修复批 A 案——见文件头注记):缺省 shared = 旧行为零变更
|
||||
var seed_mode := "shared"
|
||||
if args.size() >= 5 and args[4] in ["shared", "unique"]:
|
||||
seed_mode = args[4]
|
||||
# 基线格模式(2026-09-20 重投批工具口径):第 6 参 baseline = 只跑 atk0/hp0 一格——
|
||||
# 全网格 10 格在大 N 下成本不可行(~1.35s/局),基线判读只需零加成格的大样本精度
|
||||
var baseline_only := false
|
||||
if args.size() >= 6 and args[5] == "baseline":
|
||||
baseline_only = true
|
||||
var atk_axis: Array = [0, 50, 100, 150, 200]
|
||||
var hp_axis: Array = [0, 100]
|
||||
if baseline_only:
|
||||
atk_axis = [0]
|
||||
hp_axis = [0]
|
||||
var seed_label := "种子 %d+%d" % [seed0, n] if seed_mode == "shared" else "种子 %d 起逐局独立" % seed0
|
||||
print("=== 关卡期望扫描:level %d · N=%d/格 · %s · 策略=%s · 种子模式=%s ===" % [level_arg, n, seed_label, policy_arg, seed_mode])
|
||||
print("%-6s %-10s %-8s %-14s %-14s %-14s %-10s %-12s %-12s %-10s %-10s %-8s %-8s" % ["策略", "atk%/hp%", "通关率", "整关承伤均/p90", "通关剩余HP均(胜)", "进BossHP均", "平均轮数", "踩雷/战", "普通承伤/场%", "跳战率", "Boss承伤%", "踩雷/局", "刷新/局"])
|
||||
var perfect_mine_total := 0 # 完美档构造保证 = 零踩雷(只挖已证安全格)——扫描末断言
|
||||
for policy in policies:
|
||||
var tag := "基线" if policy == "normal" else "完美"
|
||||
for atk_pct in atk_axis:
|
||||
for hp_pct in hp_axis:
|
||||
var wins := 0
|
||||
var dmg_all: Array = []
|
||||
var hp_left_win: Array = []
|
||||
var hp_boss: Array = []
|
||||
var rounds_all: Array = []
|
||||
var mines_per_battle: Array = []
|
||||
var normal_dmg_pct: Array = [] # 每普通战斗承伤占生效 max_hp 比(难度曲线与Boss F4 口径——≤20% 带)
|
||||
var boss_dmg_pct: Array = [] # Boss 战承伤占生效 max_hp 比(难度曲线与Boss F4 口径——≤50% 带)
|
||||
var wipes := 0 # 普通战斗 round-1 即胜数(首轮全灭跳战——拍板④观测面)
|
||||
var normal_battles := 0
|
||||
var death_nodes := {}
|
||||
var mine_per_run: Array = []
|
||||
var refresh_per_run: Array = []
|
||||
for i in range(n):
|
||||
var seed_val := seed0 + i if seed_mode == "shared" else seed0 + _run_cursor
|
||||
_run_cursor += 1
|
||||
var st = autoplay.run_level(db, seed_val, level_arg, atk_pct, hp_pct, true, logger, policy)
|
||||
dmg_all.append(st.hero_dmg)
|
||||
rounds_all.append(st.rounds)
|
||||
mine_per_run.append(int(st.mine_hits))
|
||||
refresh_per_run.append(int(st.refreshes))
|
||||
mines_per_battle.append(float(st.mine_hits) / float(maxi(1, st.battles)))
|
||||
var eff_max := maxi(1, int(round(float(db.hero.max_hp) * (100.0 + float(hp_pct)) / 100.0)))
|
||||
# 逐战斗承伤切分(round_index 回卷 = 新战斗;末段 = Boss 战——当且仅当本局抵达 Boss 节点)
|
||||
var segs := _damage_segments(st.rows)
|
||||
for k in segs.size():
|
||||
var pct := 100.0 * float(segs[k].dmg) / float(eff_max)
|
||||
if k == segs.size() - 1 and int(st.hp_at_boss) >= 0:
|
||||
boss_dmg_pct.append(pct)
|
||||
else:
|
||||
normal_dmg_pct.append(pct)
|
||||
normal_battles += 1
|
||||
if bool(segs[k].r1_win):
|
||||
wipes += 1
|
||||
if st.result == "victory":
|
||||
wins += 1
|
||||
hp_left_win.append(st.hp_left)
|
||||
hp_boss.append(st.hp_at_boss)
|
||||
elif st.result == "defeat":
|
||||
var key := "%d.%s" % [st.defeat_node_index, st.defeat_node_type]
|
||||
death_nodes[key] = int(death_nodes.get(key, 0)) + 1
|
||||
var death_txt := "—"
|
||||
if not death_nodes.is_empty():
|
||||
var parts: Array = []
|
||||
for k in death_nodes.keys():
|
||||
parts.append("%s×%d" % [k, death_nodes[k]])
|
||||
death_txt = " ".join(parts)
|
||||
var mine_mean := _mean(mine_per_run)
|
||||
if policy == "perfect":
|
||||
perfect_mine_total += int(round(mine_mean * float(n)))
|
||||
print("%-6s atk%+3d%% hp%+3d%% %5.0f%% %6.1f / %-6.1f %6.1f %6.1f %6.1f %5.2f %6.1f %5.0f%% %6.1f %6.2f %6.2f %s" % [
|
||||
tag, atk_pct, hp_pct, 100.0 * float(wins) / float(n),
|
||||
_mean(dmg_all), _p90(dmg_all), _mean(hp_left_win), _mean(hp_boss), _mean(rounds_all), _mean(mines_per_battle),
|
||||
_mean(normal_dmg_pct), 100.0 * float(wipes) / float(maxi(1, normal_battles)), _mean(boss_dmg_pct), mine_mean, _mean(refresh_per_run), death_txt])
|
||||
print("(F4 带:承伤 ≤20%%/普通场 · ≤50%%/Boss 场,占生效 max_hp · F5 带:基线通关率 L1 [35,55]%% / L2 [25,45]%%——2026-09-20 用户拍板分带 · 跳战率 = 普通战斗 round-1 即胜(首轮全灭)占比 · 承伤含败者至死 · 败北节点 0-3=战斗节点序 4=Boss · 踩雷/战=每战斗负向兑付触发期望(格面资源批口径) · 完美档 = 技能轴上界测量(零盲挖/全标旗/卡死刷新——口径见 auto_player.gd)· 耗时 %.1f 秒)" % [float(Time.get_ticks_msec() - t0) / 1000.0])
|
||||
if "perfect" in policies and perfect_mine_total > 0:
|
||||
push_error("[level_monte_carlo] 完美策略档出现踩雷 %d 次——构造保证被破坏(只挖已证安全格),求解器/驱动有缺陷" % perfect_mine_total)
|
||||
quit(1)
|
||||
return
|
||||
logger.close_session()
|
||||
quit(0)
|
||||
|
||||
|
||||
# 逐战斗段切分:round_index 回卷 = 新战斗(defense_scan 同口径)
|
||||
# 每段 = {dmg: 英雄承伤合计, r1_win: round-1 即胜(首轮全灭跳战签名——verdict 胜利行 value=1)}
|
||||
func _damage_segments(rows: Array) -> Array:
|
||||
var segs: Array = []
|
||||
var last_round := 0
|
||||
for row in rows:
|
||||
var r: int = int(row.get("round_index", 0))
|
||||
if segs.is_empty() or r < last_round:
|
||||
segs.append({"dmg": 0, "r1_win": false})
|
||||
last_round = r
|
||||
var seg: Dictionary = segs[segs.size() - 1]
|
||||
if String(row.get("type", "")) == "damage" and String(row.get("source_class", "")) == "enemy_action":
|
||||
seg.dmg = int(seg.dmg) + int(row.get("value", 0))
|
||||
if String(row.get("type", "")) == "verdict" and r == 1 and int(row.get("value", 0)) == 1:
|
||||
seg.r1_win = true
|
||||
return segs
|
||||
|
||||
|
||||
func _mean(a: Array) -> float:
|
||||
if a.is_empty():
|
||||
return 0.0
|
||||
var t := 0.0
|
||||
for v in a:
|
||||
t += float(v)
|
||||
return t / float(a.size())
|
||||
|
||||
|
||||
func _p90(a: Array) -> float:
|
||||
if a.is_empty():
|
||||
return 0.0
|
||||
var b := a.duplicate()
|
||||
b.sort()
|
||||
return float(b[mini(b.size() - 1, int(0.9 * b.size()))])
|
||||
@@ -1 +0,0 @@
|
||||
uid://h3mghae7yvvx
|
||||
@@ -1,16 +1,17 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Date: 2026-09-22(数值验证工具批——判定端 tools/balance_verifier.py 的数据源,本文件零判定逻辑)
|
||||
# 三段一次采集,stdout 以「VJSON 」前缀行出机器读 JSON(其余行为人读进度/摘要):
|
||||
# level 段 直入口单关基线(level_autoplay.run_level 零加成)——L1/L2/L3 × 双画像 × n,
|
||||
# 供验收带判定(分带批 2026-09-20 口径:L1 普通 [35,55] / L2 普通 [25,45] / 完美 [35,65])
|
||||
# adventure 段 全冒险连探基线(run_adventure 零永久强化)× 双画像 × na——止步分布与败退节点
|
||||
# (摘要 defeat 明细 + 末场战斗敌方名),回答「零加成倒在第几层哪个节点」
|
||||
# 2026-09-23 单关直入口口径退役(用户拍板):原 level 段(直入口单关基线——满血零上下文进关,
|
||||
# 与真实冒险的条件通关率脱节,曾误导曲线调参)整段移除;逐关难度读数由 adventure 段行内
|
||||
# levels_cleared/stopped_at 派生(判定端 analyze_adventure_levels),零补采。
|
||||
# 两段一次采集,stdout 以「VJSON 」前缀行出机器读 JSON(其余行为人读进度/摘要):
|
||||
# adventure 段 全冒险连探基线(run_adventure 零永久强化)× 双画像 × na——止步分布、败退节点
|
||||
# (摘要 defeat 明细 + 末场战斗敌方名)与逐关条件通关率派生源
|
||||
# campaign 段 营地循环战役 × 双画像 × k 局——SaveManager 隔离档 + MetaProgression 真实账本:
|
||||
# 逐局 hero_override() 注入 → 冒险 → settle_adventure 入账 → 花费策略买锻造,
|
||||
# 回答「累计几次通关冒险后下一关可过」(元进度换算率标定的模拟读数源)
|
||||
# 运行: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(锻造花费优先序)
|
||||
# na=40(基线冒险局数) k=40(战役局数) seed0=8000
|
||||
# only=adventure,campaign(缺省全段) spend=hp_first|atk_first(锻造花费优先序)
|
||||
# 种子纪律:unique 逐局独立(掷点测量修复批 A 案——seed0 + 全局游标,每掷点恰消费一次)。
|
||||
# 日志合规:全段共享 playtest_logger 会话(与真人试玩同日志),分析工具可直接消费。
|
||||
# 存档红线:战役档用 user://verifier_campaign_<画像>.json 隔离路径,段首清陈旧段尾清理——
|
||||
@@ -38,21 +39,19 @@ func _initialize() -> void:
|
||||
print("模拟日志: %s" % log_path)
|
||||
var autoplay = load("res://smoke/level_autoplay.gd")
|
||||
var t0 := Time.get_ticks_msec()
|
||||
var counts := {"level": 0, "adventure": 0, "campaign": 0}
|
||||
if "level" in opt.only:
|
||||
_seg_level(db, autoplay, logger, opt, counts)
|
||||
var counts := {"adventure": 0, "campaign": 0}
|
||||
if "adventure" in opt.only:
|
||||
_seg_adventure(db, autoplay, logger, opt, counts)
|
||||
if "campaign" in opt.only:
|
||||
_seg_campaign(db, autoplay, logger, opt, counts)
|
||||
logger.close_session()
|
||||
print("数值验证采集完成:level %d 局 · adventure %d 局 · campaign %d 局,耗时 %.1f 秒"
|
||||
% [counts.level, counts.adventure, counts.campaign, float(Time.get_ticks_msec() - t0) / 1000.0])
|
||||
print("数值验证采集完成:adventure %d 局 · campaign %d 局,耗时 %.1f 秒"
|
||||
% [counts.adventure, counts.campaign, float(Time.get_ticks_msec() - t0) / 1000.0])
|
||||
quit(0)
|
||||
|
||||
|
||||
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 := {"na": 40, "k": 40, "seed0": 8000, "only": "adventure,campaign", "spend": "hp_first"}
|
||||
for a in args:
|
||||
var kv := String(a).split("=", true, 1)
|
||||
if kv.size() != 2:
|
||||
@@ -67,7 +66,7 @@ func _parse_args(args: PackedStringArray) -> Dictionary:
|
||||
opt[key] = int(kv[1])
|
||||
var segs: Array = []
|
||||
for s in String(opt.only).split(","):
|
||||
if s in ["level", "adventure", "campaign"]:
|
||||
if s in ["adventure", "campaign"]:
|
||||
segs.append(s)
|
||||
opt["only"] = segs
|
||||
if not SPEND_ORDERS.has(String(opt.spend)):
|
||||
@@ -85,25 +84,7 @@ func _emit(row: Dictionary) -> void:
|
||||
print("VJSON " + JSON.stringify(row))
|
||||
|
||||
|
||||
# ---- level 段:直入口单关基线(零加成——分带批验收带判定的同口径读数) ----
|
||||
|
||||
func _seg_level(db, autoplay, logger, opt: Dictionary, counts: Dictionary) -> void:
|
||||
for policy in ["normal", "perfect"]:
|
||||
for li in [1, 2, 3]:
|
||||
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")
|
||||
_emit({
|
||||
"seg": "level", "level": li, "policy": policy, "seed": seed_val,
|
||||
"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)])
|
||||
|
||||
|
||||
# ---- adventure 段:全冒险连探基线(零永久强化——止步分布与败退节点归因) ----
|
||||
# ---- adventure 段:全冒险连探基线(零永久强化——止步分布/败退节点归因/逐关条件通关率派生源) ----
|
||||
|
||||
func _seg_adventure(db, autoplay, logger, opt: Dictionary, counts: Dictionary) -> void:
|
||||
for policy in ["normal", "perfect"]:
|
||||
|
||||
+81
-75
@@ -2,17 +2,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r"""数值验证工具(判定端)——自动化评判 + 元进度爬升测量,替代「亲手试玩再反馈」循环。
|
||||
|
||||
三段采集(数据源 = smoke/verifier_batch.gd,本文件驱动它并做全部判定):
|
||||
level 段 直入口单关基线(零加成)——验收带判定
|
||||
adventure 段 全冒险连探基线(零永久强化)——止步分布 + 败退节点归因(层/节点/敌人)
|
||||
两段采集(数据源 = smoke/verifier_batch.gd,本文件驱动它并做全部判定):
|
||||
adventure 段 全冒险连探基线(零永久强化)——逐关条件通关率派生 + 止步分布 + 败退节点归因(层/节点/敌人)
|
||||
campaign 段 营地循环战役(隔离档真实元进度账本:注入→冒险→收束入账→买锻造)——爬升曲线
|
||||
|
||||
评判标准(溯源):
|
||||
① 直入口基线通关率带(分带批 2026-09-20 用户拍板,难度曲线与Boss v0.16):
|
||||
第一层 普通 [35,55]% · 第二层 普通 [25,45]% · 完美档 [35,65]%(两关同带);第三层带未定——只测不判。
|
||||
① 逐关条件通关率(冒险口径派生——打通 k-1 关才到达第 k 关,通关数/到达数):单关直入口口径
|
||||
(分带批 2026-09-20,难度曲线与Boss v0.16)满血零上下文进关,与真实冒险的条件通关率脱节,
|
||||
曾误导曲线调参——已随单关试玩入口一并退役(2026-09-23 用户拍板);旧带值
|
||||
(L1 普通 [35,55]/L2 普通 [25,45]/完美档 [35,65])锚定直入口读数不可比,
|
||||
带重锚归曲线调参批——重锚前本表只测不判。
|
||||
② 元循环目标(用户提案 2026-09-22,数字待确认):累计通过 5 次第 k 层的冒险后,第 k+1 层的
|
||||
条件通关率(滑窗)应进入该层验收带——「5 次通过第一层的冒险后能过第二层;5 次通过第二层后能过第三层」。
|
||||
第三层带未定,只给爬升曲线与首次读数,不出判定。
|
||||
条件通关率(滑窗)应进入该层验收带——判定同①随口径退役暂停(重锚前只给滑窗读数与触发点)。
|
||||
③ 魔法道具面:道具工坊六件按通关次数解锁(阈值 1/1/2/2/3/3),局内效果未实装(归 Alpha 魔法道具批)——
|
||||
本工具只报解锁时点,不入判定。
|
||||
|
||||
@@ -22,11 +23,13 @@ r"""数值验证工具(判定端)——自动化评判 + 元进度爬升测
|
||||
· 工坊阈值(meta.tres):显影尘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 # 全段默认规模
|
||||
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 --from-raw <raw.jsonl> # 免重跑,对既有采集重新判定/出报告
|
||||
输出:控制台判定结论 + production/qa/balance-verifier-<时间戳>.md(报告)+ 同名 .raw.jsonl(原始逐局数据)。
|
||||
兼容注:2026-09-23 前的旧 raw.jsonl 含 level 段(直入口单关)行——本版判定自动忽略,
|
||||
逐关读数一律由冒险行派生。
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
@@ -45,12 +48,8 @@ VJSON_PREFIX = "VJSON "
|
||||
ERROR_MARKERS = ("SCRIPT ERROR", "Parse Error")
|
||||
|
||||
# ---- 判定标准区(溯源见文件头) ----
|
||||
LEVEL_BANDS = {
|
||||
("normal", 1): (35, 55),
|
||||
("normal", 2): (25, 45),
|
||||
("perfect", 1): (35, 65),
|
||||
("perfect", 2): (35, 65),
|
||||
}
|
||||
# 直入口验收带(原 LEVEL_BANDS)已随单关直入口口径退役(2026-09-23 用户拍板)——
|
||||
# 逐关/检查点判定全部暂停(judge_band 传 band=None = 只测不判),带重锚归曲线调参批。
|
||||
CAMPAIGN_CLEARS_TARGET = 5 # 元循环检查点:累计通过第 k 层的冒险次数(用户提案 2026-09-22,待确认)
|
||||
CAMPAIGN_WINDOW = 10 # 检查点滑窗局数(条件通关率的窗口)
|
||||
CAMPAIGN_MIN_ATTEMPTS = 5 # 滑窗内少于此局数 = 数据不足(不出判定)
|
||||
@@ -81,9 +80,9 @@ def pad(s, w, right=False):
|
||||
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"na={args.na}", f"k={args.k}", f"seed0={args.seed0}",
|
||||
f"only={args.only}", f"spend={args.spend}"]
|
||||
print(f"[数值验证] 采集启动({' · '.join(segs)},约 {args.est_min} 分钟)…")
|
||||
print(f"[数值验证] 采集启动({' · '.join(segs)},约 {args.est_min:.0f} 分钟)…")
|
||||
t0 = time.time()
|
||||
try:
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8",
|
||||
@@ -133,7 +132,7 @@ def binom_ci(wins, n):
|
||||
|
||||
|
||||
def judge_band(rate_pct, band):
|
||||
"""落带判定:返回 (结论, 是否达标)。band=None = 带未定,只报读数。"""
|
||||
"""落带判定:返回 (结论, 是否达标)。band=None = 带未定(直入口口径退役后待重锚),只报读数。"""
|
||||
if band is None:
|
||||
return "带未定(只测不判)", None
|
||||
lo, hi = band
|
||||
@@ -163,28 +162,36 @@ def node_label(defeat):
|
||||
|
||||
# ---------------------------------------------------------------- 分段分析
|
||||
|
||||
def analyze_level(rows):
|
||||
"""直入口基线:按(画像, 层)聚合计数。"""
|
||||
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": {}})
|
||||
a["n"] += 1
|
||||
if r.get("result") == "victory":
|
||||
a["wins"] += 1
|
||||
elif r.get("result") == "defeat":
|
||||
k = (int(r.get("defeat_node_id", -1)), r.get("defeat_node_type", ""))
|
||||
a["deaths"][k] = a["deaths"].get(k, 0) + 1
|
||||
return agg
|
||||
def valid_adventure_runs(rows, policy):
|
||||
"""冒险段有效局(summary 在场——levels_cleared >= 0);编排异常局单列。"""
|
||||
runs = [r for r in rows if r.get("seg") == "adventure" and r.get("policy") == policy
|
||||
and int(r.get("levels_cleared", -1)) >= 0]
|
||||
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):
|
||||
到达第 k 关 ⇔ levels_cleared >= k-1;通关第 k 关 ⇔ levels_cleared >= k。
|
||||
条件通关率 = 通关数 / 到达数(L1 的到达数 = 全部有效局——冒险即从第一关开始)。"""
|
||||
out = {}
|
||||
for policy in ("normal", "perfect"):
|
||||
runs, invalid = valid_adventure_runs(rows, policy)
|
||||
levels = {}
|
||||
for k in (1, 2, 3):
|
||||
reached = sum(1 for r in runs if int(r.get("levels_cleared", 0)) >= k - 1)
|
||||
cleared = sum(1 for r in runs if int(r.get("levels_cleared", 0)) >= k)
|
||||
levels[k] = {"reached": reached, "cleared": cleared}
|
||||
out[policy] = {"n": len(runs), "invalid": invalid, "levels": levels}
|
||||
return out
|
||||
|
||||
|
||||
def analyze_adventure(rows):
|
||||
out = {}
|
||||
for policy in ("normal", "perfect"):
|
||||
runs = [r for r in rows if r.get("seg") == "adventure" and r.get("policy") == policy]
|
||||
stopped, death_nodes, l1 = {}, {}, {"n": 0, "wins": 0}
|
||||
runs, invalid = valid_adventure_runs(rows, policy)
|
||||
stopped, death_nodes = {}, {}
|
||||
for r in runs:
|
||||
sa = int(r.get("stopped_at", -1))
|
||||
if sa == 0:
|
||||
@@ -195,11 +202,7 @@ def analyze_adventure(rows):
|
||||
if d:
|
||||
lb = node_label(d)
|
||||
death_nodes[lb] = death_nodes.get(lb, 0) + 1
|
||||
if int(r.get("levels_cleared", -1)) >= 0:
|
||||
l1["n"] += 1
|
||||
if int(r.get("levels_cleared", 0)) >= 1:
|
||||
l1["wins"] += 1
|
||||
out[policy] = {"n": len(runs), "stopped": stopped, "death_nodes": death_nodes, "l1": l1}
|
||||
out[policy] = {"n": len(runs), "invalid": invalid, "stopped": stopped, "death_nodes": death_nodes}
|
||||
return out
|
||||
|
||||
|
||||
@@ -234,7 +237,7 @@ def analyze_campaign(rows):
|
||||
continue
|
||||
series = campaign_series(runs)
|
||||
res["series"] = series
|
||||
# 检查点判定:累计通过第 k 层达目标后,第 k+1 层滑窗条件通关率 vs 验收带
|
||||
# 检查点:累计通过第 k 层达目标后,第 k+1 层滑窗条件通关率——带待重锚(直入口口径退役)只报读数
|
||||
checkpoints = []
|
||||
for k in (1, 2):
|
||||
clears = 0
|
||||
@@ -257,8 +260,7 @@ 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))
|
||||
cp["verdict"], cp["pass"] = judge_band(rate, band)
|
||||
cp["verdict"], cp["pass"] = judge_band(rate, None) # 带重锚归曲线调参批
|
||||
cp["forge_at"] = chunk[0][2].get("override", {})
|
||||
checkpoints.append(cp)
|
||||
res["checkpoints"] = checkpoints
|
||||
@@ -294,25 +296,30 @@ def report(args, rows, meta):
|
||||
A = lines.append
|
||||
A(f"# 数值验证报告({meta['ts']})")
|
||||
A("")
|
||||
A(f"- 采集规模:直入口每格 {args.n} 局 · 基线冒险 {args.na} 局/画像 · 战役 {args.k} 局/画像"
|
||||
A(f"- 采集规模:基线冒险 {args.na} 局/画像 · 战役 {args.k} 局/画像"
|
||||
f" · 种子 {args.seed0} 起逐局独立 · 锻造花费策略 = {args.spend}")
|
||||
A(f"- 切片版本:{meta['commit']} · 表状态:判定带与表值溯源见 tools/balance_verifier.py 文件头")
|
||||
A(f"- 切片版本:{meta['commit']} · 口径:逐关读数 = 冒险条件口径(单关直入口口径已退役 2026-09-23,"
|
||||
f"带待重锚只测不判——重锚归曲线调参批)")
|
||||
A("")
|
||||
|
||||
# ① 直入口基线
|
||||
A("## ① 直入口基线通关率(验收带判定)")
|
||||
# ① 逐关条件通关率(冒险口径派生)
|
||||
A("## ① 逐关条件通关率(冒险口径——打通 k-1 关才到达第 k 关;带待重锚只测不判)")
|
||||
A("")
|
||||
lvl = analyze_level(rows)
|
||||
alv = analyze_adventure_levels(rows)
|
||||
tbl = []
|
||||
for (policy, li), a in sorted(lvl.items()):
|
||||
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])
|
||||
A(fmt_table(["层", "画像", "通关/局数", "通关率±95%CI", "验收带", "判定"], tbl))
|
||||
for policy in ("normal", "perfect"):
|
||||
d = alv[policy]
|
||||
cn = "普通档" if policy == "normal" else "完美档"
|
||||
for k in (1, 2, 3):
|
||||
lv = d["levels"][k]
|
||||
if lv["reached"] <= 0:
|
||||
tbl.append([LEVEL_CN[k], cn, f"—/{d['n']}", "—(未到达)", "未定"])
|
||||
continue
|
||||
rate = 100.0 * lv["cleared"] / lv["reached"]
|
||||
ci = binom_ci(lv["cleared"], lv["reached"]) * 100
|
||||
tbl.append([LEVEL_CN[k], cn, f"{lv['cleared']}/{lv['reached']}",
|
||||
f"{rate:.1f}% ±{ci:.1f}", "未定"])
|
||||
A(fmt_table(["层", "画像", "通关/到达", "条件通关率±95%CI", "验收带"], tbl))
|
||||
A("")
|
||||
|
||||
# ② 冒险基线:止步分布与败退节点
|
||||
@@ -326,8 +333,6 @@ def report(args, rows, meta):
|
||||
A("")
|
||||
stop_line = "、".join(f"{k} {v}({100.0 * v / max(1, d['n']):.0f}%)" for k, v in sorted(d["stopped"].items()))
|
||||
A(f"- 止步分布:{stop_line or '—'}")
|
||||
l1r = 100.0 * d["l1"]["wins"] / max(1, d["l1"]["n"])
|
||||
A(f"- 第一层通过率(冒险口径交叉核对):{l1r:.1f}%")
|
||||
A("- 败退节点分布(失败局归因):")
|
||||
for lb, c in sorted(d["death_nodes"].items(), key=lambda x: -x[1]):
|
||||
A(f" - {lb} ×{c}")
|
||||
@@ -366,7 +371,6 @@ def report(args, rows, meta):
|
||||
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)}",
|
||||
@@ -389,15 +393,18 @@ def report(args, rows, meta):
|
||||
|
||||
def console_verdict(args, rows):
|
||||
"""控制台判定结论(先读数后细节)。"""
|
||||
lvl = analyze_level(rows)
|
||||
alv = analyze_adventure_levels(rows)
|
||||
print("\n判定结论(detail 见报告文件)")
|
||||
for (policy, li), a in sorted(lvl.items()):
|
||||
rate = 100.0 * a["wins"] / max(1, a["n"])
|
||||
band = LEVEL_BANDS.get((policy, li))
|
||||
verdict, ok = judge_band(rate, band)
|
||||
mark = {True: "✓", False: "✗", None: "·"}[ok]
|
||||
print(f" [{mark}] 直入口{LEVEL_CN[li]}{'普通档' if policy == 'normal' else '完美档'}:"
|
||||
f"{rate:.1f}%({a['wins']}/{a['n']})→ {verdict}")
|
||||
for policy in ("normal", "perfect"):
|
||||
d = alv[policy]
|
||||
cn = "普通档" if policy == "normal" else "完美档"
|
||||
cells = []
|
||||
for k in (1, 2, 3):
|
||||
lv = d["levels"][k]
|
||||
if lv["reached"] > 0:
|
||||
rate = 100.0 * lv["cleared"] / lv["reached"]
|
||||
cells.append(f"{LEVEL_CN[k]} {rate:.1f}%({lv['cleared']}/{lv['reached']})")
|
||||
print(f" [·] 逐关条件通关率·{cn}:{' · '.join(cells) or '—'}(冒险口径——带待重锚只测不判)")
|
||||
adv = analyze_adventure(rows)
|
||||
for policy in ("normal", "perfect"):
|
||||
d = adv[policy]
|
||||
@@ -425,19 +432,18 @@ def main():
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
ap = argparse.ArgumentParser(description="数值验证工具(判定端)")
|
||||
ap.add_argument("--godot", default=GODOT)
|
||||
ap.add_argument("--n", type=int, default=100, help="直入口每格局数(±95%CI 约 ±9 点——带边判读须看 CI)")
|
||||
ap.add_argument("--na", type=int, default=60, help="基线冒险局数/画像")
|
||||
ap.add_argument("--na", type=int, default=60, help="基线冒险局数/画像(±95%CI 约 ±9 点——判读须看 CI)")
|
||||
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("--only", default="adventure,campaign")
|
||||
ap.add_argument("--spend", default="hp_first", choices=["hp_first", "atk_first"])
|
||||
ap.add_argument("--timeout", type=int, default=5400)
|
||||
ap.add_argument("--quick", action="store_true", help="小规模快跑(12/8/10)")
|
||||
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.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
|
||||
args.na, args.k = 8, 10
|
||||
args.est_min = (2 * args.na * 3.5 + 2 * args.k * 4.0) / 60
|
||||
|
||||
ts = time.strftime("%Y%m%d-%H%M%S")
|
||||
if args.from_raw:
|
||||
|
||||
Reference in new issue
Block a user