From d5783a5740cb9a785aa7bcbba58a958419c37186 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Wed, 23 Sep 2026 05:24:13 +0800 Subject: [PATCH] =?UTF-8?q?refactor(tools):=20=E6=B5=8B=E9=87=8F=E9=9D=A2?= =?UTF-8?q?=E5=8E=BB=E5=8D=95=E5=85=B3=E7=9B=B4=E5=85=A5=E5=8F=A3=E2=80=94?= =?UTF-8?q?=E2=80=94=E9=9A=BE=E5=BA=A6=E8=AF=BB=E6=95=B0=E6=94=B9=E5=86=92?= =?UTF-8?q?=E9=99=A9=E6=9D=A1=E4=BB=B6=E5=8F=A3=E5=BE=84=EF=BC=88=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E6=8C=87=E4=BB=A4=202026-09-23=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - verifier_batch.gd: level 段(直入口单关基线 360 局/全量)整段移除——满血零上下文进关 与真实冒险条件通关率脱节(直入口 L2 普通 5% vs 营地检查点 L2 条件 90% 同期并存), 曾误导曲线调参;逐关读数由 adventure 段行内 levels_cleared/stopped_at 派生,零补采 - balance_verifier.py: 直入口验收带(LEVEL_BANDS)退役,新增①逐关条件通关率表 (通关数/到达数,±95%CI,带待重锚只测不判——重锚归曲线调参批);营地检查点判定同批 暂停(旧带值锚直入口读数不可比);--n 参数随段删除;--from-raw 兼容旧 raw(level 行忽略) - 退役 level_monte_carlo.gd + level_debug.gd(单关扫描/单关诊断——同口径产 misleading 数据, 替代面 = balance_verifier 冒险段 / adventure_monte_carlo) - level_autoplay 头注:run_level 降格机制夹具/单战锚点专用——通关率/难度类测量禁用 - 保留面(非测量,零改动):smoke_all 机制夹具(治疗数学/选路状态机/L2 首遇战 12 种子锚行)、 rhythm_batch/defense_scan/damage_probe 单战机制扫描——断言对象是具体机制非通关率数据 - 文档同步:难度曲线与Boss v0.19(带口径退役注记:调参表行+验收标准行+版本历史)/ entities.yaml(clear_rate_target 两轮过期带值对齐退役注记+tool_carrier 换主载体)/ CLAUDE.md(工具行两处)/ README(headless 工具节)/ 07-体验验证清单 / docs/code-conventions §5.2 / board_stat_probe 注释 - 验证:py_compile 过;冒烟 553 项 0 失败三闸全过;balance_verifier --quick 端到端 36 局 2.7 分钟出报告(完美档逐关 87.5%→42.9%→0% 冒险条件口径首读数);doc 三闸失败集与 master 逐项一致(lint 8/死链 7/term 1 均既有) --- CLAUDE.md | 4 +- design/gdd/07-体验验证清单.md | 2 +- design/gdd/systems/冒险循环/难度曲线与Boss.md | 5 +- design/registry/entities.yaml | 6 +- docs/code-conventions.md | 2 +- .../dungeon-sweeper-vertical-slice/README.md | 12 +- .../smoke/board_stat_probe.gd | 2 +- .../smoke/level_autoplay.gd | 9 +- .../smoke/level_debug.gd | 34 ---- .../smoke/level_debug.gd.uid | 1 - .../smoke/level_monte_carlo.gd | 169 ------------------ .../smoke/level_monte_carlo.gd.uid | 1 - .../smoke/verifier_batch.gd | 47 ++--- tools/balance_verifier.py | 156 ++++++++-------- 14 files changed, 119 insertions(+), 331 deletions(-) delete mode 100644 prototypes/dungeon-sweeper-vertical-slice/smoke/level_debug.gd delete mode 100644 prototypes/dungeon-sweeper-vertical-slice/smoke/level_debug.gd.uid delete mode 100644 prototypes/dungeon-sweeper-vertical-slice/smoke/level_monte_carlo.gd delete mode 100644 prototypes/dungeon-sweeper-vertical-slice/smoke/level_monte_carlo.gd.uid diff --git a/CLAUDE.md b/CLAUDE.md index 1107bfd4..2773b0bb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/design/gdd/07-体验验证清单.md b/design/gdd/07-体验验证清单.md index 9ed0de76..bac582b8 100644 --- a/design/gdd/07-体验验证清单.md +++ b/design/gdd/07-体验验证清单.md @@ -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 日志含池值与快照。 diff --git a/design/gdd/systems/冒险循环/难度曲线与Boss.md b/design/gdd/systems/冒险循环/难度曲线与Boss.md index 27835a46..cff274b9 100644 --- a/design/gdd/systems/冒险循环/难度曲线与Boss.md +++ b/design/gdd/systems/冒险循环/难度曲线与Boss.md @@ -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 冒险段派生:通关数/到达数);新带值重锚归曲线调参批,重锚前只测不判;调参表「基线通关率带」行与验收标准通关率带条同步改口径退役注记。行值零改动 | diff --git a/design/registry/entities.yaml b/design/registry/entities.yaml index bf7d9843..07252ae7 100644 --- a/design/registry/entities.yaml +++ b/design/registry/entities.yaml @@ -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 diff --git a/docs/code-conventions.md b/docs/code-conventions.md index 6b6e2282..6bda2ea3 100644 --- a/docs/code-conventions.md +++ b/docs/code-conventions.md @@ -118,7 +118,7 @@ ("可选类 3.32"不加"每板",会被读成每格期望 >1)。 ### 5.2 调参先测量,改后复测 -改任何玩法数值前,先用扫描工具产出基线(`level_monte_carlo.gd` / `reward_expectation.gd`); +改任何玩法数值前,先用扫描工具产出基线(通关率/难度类读数走 `balance_verifier.py` 冒险段——单关直入口口径已退役,机制类读数走 `reward_expectation.gd` 等); 改完必须复测回填数据。禁止拍脑袋调参。 ### 5.3 数值设计必须跨尺度推演 diff --git a/prototypes/dungeon-sweeper-vertical-slice/README.md b/prototypes/dungeon-sweeper-vertical-slice/README.md index dd84aa5c..4f5a10ad 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/README.md +++ b/prototypes/dungeon-sweeper-vertical-slice/README.md @@ -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 确认) diff --git a/prototypes/dungeon-sweeper-vertical-slice/smoke/board_stat_probe.gd b/prototypes/dungeon-sweeper-vertical-slice/smoke/board_stat_probe.gd index ac146da0..7f75e7de 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/smoke/board_stat_probe.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/smoke/board_stat_probe.gd @@ -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 diff --git a/prototypes/dungeon-sweeper-vertical-slice/smoke/level_autoplay.gd b/prototypes/dungeon-sweeper-vertical-slice/smoke/level_autoplay.gd index 95eba40c..6bd14add 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/smoke/level_autoplay.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/smoke/level_autoplay.gd @@ -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 diff --git a/prototypes/dungeon-sweeper-vertical-slice/smoke/level_debug.gd b/prototypes/dungeon-sweeper-vertical-slice/smoke/level_debug.gd deleted file mode 100644 index ca7a3642..00000000 --- a/prototypes/dungeon-sweeper-vertical-slice/smoke/level_debug.gd +++ /dev/null @@ -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) diff --git a/prototypes/dungeon-sweeper-vertical-slice/smoke/level_debug.gd.uid b/prototypes/dungeon-sweeper-vertical-slice/smoke/level_debug.gd.uid deleted file mode 100644 index 3656d79b..00000000 --- a/prototypes/dungeon-sweeper-vertical-slice/smoke/level_debug.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bflo5h8xysqsl diff --git a/prototypes/dungeon-sweeper-vertical-slice/smoke/level_monte_carlo.gd b/prototypes/dungeon-sweeper-vertical-slice/smoke/level_monte_carlo.gd deleted file mode 100644 index e922ad9b..00000000 --- a/prototypes/dungeon-sweeper-vertical-slice/smoke/level_monte_carlo.gd +++ /dev/null @@ -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()))]) diff --git a/prototypes/dungeon-sweeper-vertical-slice/smoke/level_monte_carlo.gd.uid b/prototypes/dungeon-sweeper-vertical-slice/smoke/level_monte_carlo.gd.uid deleted file mode 100644 index b5b3319a..00000000 --- a/prototypes/dungeon-sweeper-vertical-slice/smoke/level_monte_carlo.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://h3mghae7yvvx diff --git a/prototypes/dungeon-sweeper-vertical-slice/smoke/verifier_batch.gd b/prototypes/dungeon-sweeper-vertical-slice/smoke/verifier_batch.gd index 0b716e32..11b6ea60 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/smoke/verifier_batch.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/smoke/verifier_batch.gd @@ -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"]: diff --git a/tools/balance_verifier.py b/tools/balance_verifier.py index 3f0119a4..4105f029 100644 --- a/tools/balance_verifier.py +++ b/tools/balance_verifier.py @@ -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 # 免重跑,对既有采集重新判定/出报告 输出:控制台判定结论 + 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: