From 09862615e630e8b9af84406f2695ab9535f1d390 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Mon, 28 Sep 2026 20:04:35 +0800 Subject: [PATCH] =?UTF-8?q?balance(m3-iter5-T3):=20=E6=9D=A0=E6=9D=86?= =?UTF-8?q?=E7=BD=91=E6=A0=BC=E6=89=AB=E6=8F=8F+=E5=9B=9B=E7=BA=A7?= =?UTF-8?q?=E9=98=B6=E6=A2=AF=E8=90=BD=E6=A1=A3=E2=80=94=E2=80=94=E9=85=8D?= =?UTF-8?q?=E5=AF=B9=E7=A7=8D=E5=AD=9010=E9=85=8D=E7=BD=AE(OFAT=E5=85=AD+C?= =?UTF-8?q?1/C2/C3+B0):L2=E5=8F=AF=E6=94=B6=E5=B8=A6(C3=E7=B1=BB=3D?= =?UTF-8?q?=E5=B2=A9=E6=B5=861+plb0+=E5=8D=93=E5=B0=94100+percent4?= =?UTF-8?q?=E2=86=92=E6=99=AE=E9=80=9AL2=2024%=E5=B8=A6=E7=BC=98/=E5=AE=8C?= =?UTF-8?q?=E7=BE=8EL2=2056.2%=E5=85=A5=E5=B8=A6);=E5=AE=8C=E7=BE=8EL1?= =?UTF-8?q?=E5=85=A8=E7=BD=91=E6=A0=BC89%=E9=92=89=E6=AD=BB(=E4=B8=89?= =?UTF-8?q?=E9=80=89=E4=B8=80=E5=BE=85=E7=94=A8=E6=88=B7);=E7=BE=A4?= =?UTF-8?q?=E6=88=98=E4=B8=8A=E6=8E=A2=E8=A2=ABunit=5Fcount=5Fmax=E5=9F=9F?= =?UTF-8?q?=E5=AE=88=E5=8D=AB=E5=B0=81=E6=AD=BB;r=5Fboss=E9=94=9A=E6=BC=82?= =?UTF-8?q?=E7=A7=BB=E7=99=BB=E8=AE=B0(L2/L3=E2=89=880.33<=E5=9F=9F?= =?UTF-8?q?=E4=B8=8B=E9=99=90);=E5=AF=B9=E8=B4=A6=E4=BB=B6=E5=9B=9B?= =?UTF-8?q?=E9=A1=B9=E9=87=8D=E8=B7=91=E8=AE=B0=E5=BD=95(=E6=8A=A4?= =?UTF-8?q?=E6=A0=8FB/=E6=8C=96=E6=8E=98=E4=B8=8D=E7=AD=89=E5=BC=8F?= =?UTF-8?q?=E9=87=8D=E7=AB=8B/sapped-heal=E5=BD=93=E9=87=8F/=E6=8D=A2?= =?UTF-8?q?=E7=AE=97=E7=8E=87=E9=94=9A=E5=A4=8D=E9=AA=8C)+=E5=88=B7?= =?UTF-8?q?=E6=96=B0=E9=98=88=E5=80=BC=E6=8F=90=E6=A1=88;=E8=A1=8C?= =?UTF-8?q?=E5=80=BC=E9=9B=B6=E5=8F=98=E6=9B=B4=E5=BE=85=E5=88=A4=E8=AF=BB?= =?UTF-8?q?(U1+k+=E5=AE=8C=E7=BE=8EL1=E4=B8=89=E9=80=89=E4=B8=80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design/gdd/systems/冒险循环/难度曲线与Boss.md | 17 ++- design/qa/m3-iter5-lever-scan-2026-09-28.md | 81 +++++++++++ sim-data/t3_scan_results.jsonl | 10 ++ tools/t3_lever_scan.py | 137 ++++++++++++++++++ 4 files changed, 243 insertions(+), 2 deletions(-) create mode 100644 design/qa/m3-iter5-lever-scan-2026-09-28.md create mode 100644 sim-data/t3_scan_results.jsonl create mode 100644 tools/t3_lever_scan.py diff --git a/design/gdd/systems/冒险循环/难度曲线与Boss.md b/design/gdd/systems/冒险循环/难度曲线与Boss.md index 7fe08922..577a1c7e 100644 --- a/design/gdd/systems/冒险循环/难度曲线与Boss.md +++ b/design/gdd/systems/冒险循环/难度曲线与Boss.md @@ -120,6 +120,19 @@ MVP 行值并不逐式现算,而是按此式量级直填经切片验证过的 第三关口径(第三关内容批):L3 全部行值 provisional-pending-growth,上述各带对 L3 暂不适用——零成长基线下的红档属预期(主角成长侧前置 = GP+商店房批,已于 2026-09-20 落地:GP 掉落 + 商店房 + 格面金币),现行冒烟的首遇绿档断言按「成长代理档 atk+200 / hp+100」口径执行;正式通关率带与首遇绿档口径随玩家成长落地换装,归曲线调参批统一收带。 +**判读口径同步(M-3 迭代 5 基线重测批,2026-09-28)**:通关率带的测量口径 = 冒险条件通关率(打通 k−1 关才到达第 k 关,balance_verifier 冒险段派生;单关直入口口径已随 2026-09-23 拍板退役)——带值沿用 2026-09-20 分带修正(普通档 L1 [35,55] / L2 [25,45]、完美档 [35,65];M-3 迭代 5 计划书 C4 冻结:改带=用户拍板)。上方行 115 的「带退役待重锚、重锚前只测不判」注记按本条同步理解:迭代 5 基线重测已按现行带对冒险条件口径判读(读数见 `design/qa/m3-iter5-baseline-2026-09-28.md`);是否在新口径上重取带值,归迭代 5 出口判读由用户拍板,本批不动带数字。 + +### 越带回滚阶梯(四级阶梯,2026-09-28 落档——自 acceptance-ap6-ac v1.0 §e 收编) + +读数越带时的处置阶梯,粒度细→粗、爆炸半径小→大,**双向可用**(承伤偏低加深压力、承伤偏高放宽折扣): + +1. **实现缺陷检查(AC 档位边界)**——先查双重乘算、档位映射反向等缺陷,对照掷点面 EV 减免读数定位,再谈调参; +2. **AC 档位值**——掷点折扣/暴击档位行值(读 `settlement.tres`); +3. **难度曲线行值加难**——本系统行值(血量锚走廊、变异 effect_value/percent_of_cells/per_level_bonus、群战数量、注入衰减等已登记杠杆位); +4. **行动点 6→4**——量程最大、爆炸半径最大(AP 已于压速批落 6,第四级启用即推翻压速批拍板,须用户裁决)。 + +四条边界:①「承伤偏高(减免过度)」败向先查实现缺陷再谈调参;②动用行值级或行动点级均使同种子历史坐标整体作废,须重立基线、另起验收批;③敌方 AC 正式值不进本链(归怪物设定批——M-3 迭代 5 T4 通道);④每次调参后全量重跑验收并在调参日志留联动检算记录(禁独立调参网络纪律)。 + ### 商店与 GP 经济杠杆:玩家侧供给 × 定价的第三族杠杆 GP(显示名「金币」)与商店房随 2026-09-20 GP+商店房批落地,给难度引入第三族玩家侧杠杆:**GP 供给 × 商品定价 = 入关属性参数**——供给或定价单边移动,玩家入关时的攻击 / 生命 / 护甲与通关率随之漂移,与血量锚走廊、变异 effect_value 同列进禁独立调参网络。 @@ -158,7 +171,7 @@ boss_hp ≈ round( Σ普通战斗敌方生命期望 × r_boss ) r_boss |---|---|---| | Σ普通战斗敌方生命期望 | 按数量区间期望逐行合计(区间中点),行值读敌人表 | 全关非 Boss 节点敌方生命合计;逐数闭合随调参批在调参日志留档 | | r_boss | 域 [0.5, 1.1] | Boss 生命 = Σ × r_boss;Boss 行值读敌人表(provisional) | -| 加难走廊 | [1.1, 1.35] | 通关率压带的首选杠杆候选;扫描未执行,处 provisional-pending-scan(见「待办」) | +| 加难走廊 | [1.1, 1.35] | 通关率压带的首选杠杆候选;**扫描已执行(M-3 迭代 5,2026-09-28)**——域内取样(r_boss 0.8 档)对完美档 Boss 战无感(胜率 98% 血量加压打不到)、对普通档 −6pp;走廊段(>1.1,越域)未启用——启用即越域须登记(读数明细见 `design/qa/m3-iter5-lever-scan-2026-09-28.md`) | Σ 的逐数闭合口径:数量走开战掷骰的区间期望(区间中点),教学位同样掷骰、按区间期望计入;各坐标行值一律读敌人表,本文不重复登记具体数值——防与配置双写失同步。 @@ -266,7 +279,7 @@ Boss 战的棋盘用本关最大板形(板形行值读棋盘参数表),板 | 参数 | 取值域 / 口径 | 说明 | |---|---|---| -| 血量锚比率 r_boss | [0.5, 1.1];加难走廊 [1.1, 1.35] 待扫描 | Boss 生命锚 | +| 血量锚比率 r_boss | [0.5, 1.1];加难走廊 [1.1, 1.35](扫描已执行,见「Boss 设计」节) | Boss 生命锚;**锚漂移登记(迭代 5 基线重测批)**:数值设计批 Boss 血轴拔高后 L2/L3 现值 r_boss≈0.33(全行集 Σ 口径)低于域下限 0.5——复算与是否收锚归迭代 5 出口判读 | | 克制矩阵 | 克制 / 中性 / 被克三档加法修正(+1/0/−1 点),读 `settlement.tres` 甲效表 | 消费归[战斗结算](../战斗循环/战斗结算.md) | | 完美排雷乘数 | 读表 | 调参权本文 | | 当量参考敞口 | 读表 | 变更触发全量对账重跑 | diff --git a/design/qa/m3-iter5-lever-scan-2026-09-28.md b/design/qa/m3-iter5-lever-scan-2026-09-28.md new file mode 100644 index 00000000..decf97a5 --- /dev/null +++ b/design/qa/m3-iter5-lever-scan-2026-09-28.md @@ -0,0 +1,81 @@ +# M-3 迭代 5 杠杆网格扫描报告(T3) + +- **日期**:2026-09-28 +- **被测构建**:master @ 20533e3 + T1 基线(无行值改动——本报告全部读数为**补丁即跑即还原**的扫描读数,现行数据表零变更) +- **工具**:`tools/t3_lever_scan.py`(本批批内扫描入口——对 `variation.tres`/`enemy.tres` 打锚文本补丁 → 跑 `adventure_monte_carlo.gd` → git checkout 还原);原始逐配置读数 `sim-data/t3_scan_results.jsonl`(10 配置) +- **方法**:配对种子(全部配置同种子窗 73000,unique 逐局独立,N=100/策略/配置)——配置间差分 = 配对增量,消种子方差;判定带 = C4 冻结现行带(普通 L1 [35,55]/L2 [25,45]、完美 [35,65]) +- **过程事故登记**:B0 首跑与 P2 并发执行致两者读到同一份补丁表(读数逐位相同=作废)——扫描配置禁并发的教训入账;B0 已单跑重测(本报告以重测值为准) + +## 一、扫描网格与读数(N=100/策略,配对种子 73000) + +### 配对基线 B0(现行表值) + +| | 普通 L1 | 普通 L2 | 完美 L1 | 完美 L2 | +|---|---|---|---|---| +| B0 读数 | 50.0% | 4.0% | 89.0% | 15.7% | +| 现行带 | [35,55] ✓ | [25,45] ✗ | [35,65] ✗ | [35,65] ✗ | + +### OFAT 单杠杆增量(对 B0 配对差分,单位 pp) + +| 探针 | 杠杆位 | Δ普通L1 | Δ普通L2 | Δ完美L1 | Δ完美L2 | 定性 | +|---|---|---|---|---|---|---| +| P1 | 岩浆 effect_value 2→1 | 0 | **+14.0** | 0 | +13.5 | L2 减难·强 | +| P2 | per_level_bonus 2→0 | 0 | **+14.0** | 0 | +15.8 | L2/L3 减难·强(L3 0→21.4%) | +| P3 | L2 Boss 卓尔 130→100 | 0 | +4.0 | 0 | +5.6 | L2 减难·弱 | +| P4 | 荆棘 effect_value 1→2 | −2 | +0.2 | **−7** | −1.1 | L1 加难·弱 | +| P5 | L1 Boss 酋长 72→86(r_boss 0.67→0.80 域内) | −6 | +0.5 | −1 | −4.3 | 只咬普通档(完美 L1 Boss 胜率 98%,血量加压打不到它) | +| P6 | L1 群战上探(一伙/大地精 max 3→4) | — | — | — | — | **结构性不可用**:ConfigDB 跨表校验硬上限 unit_count_max 域 [unit_count, 3],抬上限=改校验域,超本批授权 | + +### 组合配置 + +| 配置 | 普通L1 | 普通L2 | 完美L1 | 完美L2 | 判 | +|---|---|---|---|---|---| +| C1 = P1+P2 | 50.0 ✓ | 22.0 ✗(−3) | 89.0 ✗ | 43.8 ✓ | 完美 L2 入带 | +| C2 = C1+P4+P5(注册杠杆全开) | 41.0 ✓ | 22.0 ✗(−3) | **81.0 ✗** | 49.4 ✓ | 全网格完美 L1 下限 | +| C3 = C1+P3+P7(percent 5→4) | 50.0 ✓ | **24.0**(带缘 −1,±4.4 CI 内) | 89.0 ✗ | **56.2 ✓** | 3/4 入带最优解 | +| C3 侧效应 | L3 无带:普通 0→16.7%、完美 0→34.0%(plb0+percent4 使 L3 变异密度 9%→4%) | | | | L3 带外注意项 | + +## 二、扫描结论 + +1. **完美档 L1 全网格不可达**:89.0% 在全部 L2 减难杠杆下零位移(P1/P2/P3/P7 均不触 L1 环节);L1 定向杠杆全部弱(荆棘 +1 = −7pp 上限、酋长 +14 血对完美无感、群战上探被校验域封死)——注册杠杆集内 81%(C2)即完美 L1 下限,距带 [35,65] 上缘差 16pp。**完美 L1 的强势来自 10×10 板形 × 完美画像挖掘效率 × 奖励经济学,不在任何已注册旋钮的量程内**。 +2. **L2 双画像可收带**:C3 类组合(岩浆 2→1 + per_level_bonus 2→0 + 卓尔 130→100 + percent_of_cells 5→4)把普通 L2 送到带缘 24.0%(差 1pp,N=100 噪声带内)、完美 L2 56.2% 稳入带。 +3. **L3 侧效应**:L2 减难杠杆同时大幅抬升 L3 通关率(完美 0→34%)——L3 无带不判,但 L3 行值 provisional-pending-growth 的再校准(灰矮人矿城血轴)在判读后若采纳 C3 类组合将需要跟进(另批或同批追加行,非本扫描范围)。 +4. **工程发现**:r_boss 锚漂移——数值设计批 Boss 血轴拔高后,L2/L3 现值 r_boss≈0.33(Σ普通战生命期望=389/415,全行集口径)低于注册域 [0.5,1.1] 下限(L1 0.67 在域内);复算与是否收锚归出口判读。 + +## 三、伴生对账件重跑记录(随批义务) + +### ① 清图支配性护栏 B 重跑 ✅(tools/clear_dominance_audit.py,现行 .tres 值链) + +- L1 前段·实测(10×10 统一板):完美档 Δ_clear 首次翻正(+8.3/+5.5/+2.7,u=1/2/3)——棋盘统一批后完美档在前段板清图挖掘已过支配线; +- L1 后段·实测 / L2·实测:仍全线 ✗(完美档缺口 −6.6 ~ −34.2 当量)——缺口形态与 2026-09-19 基线记录同构(当期记录 L1≈24/L2≈45),板形统一收窄了前段、未翻转后段; +- 护栏 B(防钓鱼上界):D_ref=4 下 C=13 vs 上界 10(|S|min=50)越 1.3×,|S|min=60 带 ✓——**越线处置三选一仍挂用户**(登记债务,本批不动数字)。 + +### ② 挖掘风险不等式(重立记录)✅ + +现行惩罚链(八型批换装后):雷格负向=踩雷直伤(flat 主角单据)+群伤负向=警觉(敌方 AC +1 掷点折扣分布型);当量换算走效果档位对照表(flat E=表值;AC 轴=点数×5pp×D_ref=0.2 当量/次攻击)。重立结论:**风险端定价对收益端系统性不足的状态未闭合**(与清图支配性 L1 后段/L2 ✗ 区同源——挖掘收益端在板形统一与奖励密度放大后走强);修正位=雷格惩罚行值/负向权重池,**不在本批注册杠杆集**(属新杠杆授权),债务登记维持、行值不动。 + +### ③ heal 负向直伤 + 乏力 sapped 量级当量 ✅ + +- heal 负向直伤现行 power=1:当量换算表 flat 类 E=表值=1 = 0.25×D_ref(一次基准攻击的 1/4)——与荆棘 1/岩浆 2/踩雷直伤同处 flat 小整数档(C1 加法核一致),**当量自洽,无越带面**(该行值无注册带);2026-09-17 压缩批(−4→−1)的量级锚对账义务以本记录履行。 +- 乏力 sapped(−25% 输出一拍):percent 类 E=|层数×行值|÷100×D_ref=1.0 当量/结算——与 flat 一档同量级,轴一致。**已知债(不在本批)**:sapped 经 hero_offense 池的乘区接线缺口(格面调研「sapped 死效果」修复报价)仍挂格面差异化批,量级当量不受其影响。 + +### ④ 换算率锚联动复验 ✅ + +6/9/1(meta.tres conversion)在 T1 verifier campaign 段实测驱动正常:注入逐档生效无断链、爬升曲线健康(完美档 ~21 局/普通档 ~36–51 局进稳态通关,检查点表值平移 +21/+38 随触发自动生效)。锚定时效注记:6/9/1 标定于 2026-09-23(L2 收紧后旧口径);本批 L2 若按 C3 类收带(变易),爬升将进一步加速——**是否重标归出口判读**(护甲行强条目纪律:重测须按解锁后参数实测=verifier campaign 段即实测面)。 + +### ⑤ 刷新经济阈值基线读数 ✅(T1 交付,阈值提案随判读锁) + +基线(T1):完美档 1.95 次/局·弃图率 19%·每轮刷新率 ≈4.7%(2026-09-19 基线 0.23 次/局·≤0.6% 的 ~30 倍位移——弃图重摇已进完美画像策略核心);普通档全零。**阈值提案(判读锁)**:预警阈值=弃图率 >30% 或每轮刷新率 >10%(≈2× 现行基线带外报警);现行值记「高弃图常态」观察态。 + +## 四、判读待决(停机等待面) + +本批行值**零变更**(扫描=补丁即还原)——收带投值(provisional→正式)卡两点: + +1. **U1 真人局未到账**:k 系数(真人整关承伤中位 ÷ 模拟完美档中位 55.0,预警线 k<0.3)未合入——06「加难杠杆扫描」条目的判读门=真人批 k 系数。采集指引=07⑨(零天赋档 3–5 局,日志自动落盘)。 +2. **完美档 L1 无解需用户裁决**(三选一,与 U1 无关、独立待拍): + - 甲:**新杠杆授权**——敌方攻击基线上探(acceptance-ap6-ac f① 候选中有、06 条目注册表未收)或 L1 专属加难内容(新杠杆,须走杠杆注册); + - 乙:**带修正**——完美档 L1 带上缘 65 上调(改带=用户拍板,C4 让位); + - 丙:**接受 89% 挂账**——完美画像定位为上界仪(非真人画像),其 L1 越带不判、优先保 L2 双画像收带(本扫描读数支持)。 +3. C3 类组合(若判读采纳)的行值清单:`variation.tres` 岩浆 effect_value 2→1、per_level_bonus 2→0、percent_of_cells 5→4;`enemy.tres` (2,9) 卓尔 hp 130→100;伴随义务=registry 同步 + L2 池复验(T4 AC 投值前先锁本组合,防两轴混淆归因)+ 小样 20 局→全量确认 + 侧效应 L3 记录。 + +> 扫描完成即 T3 机器面全毕;判读=停机点(计划书 §9-U1 等待 + 完美 L1 三选一)。等待期间 T4(AC 投值)/T5(节点图生成,实施门=调参收带后)按串行纪律顺延。 diff --git a/sim-data/t3_scan_results.jsonl b/sim-data/t3_scan_results.jsonl new file mode 100644 index 00000000..d7acd898 --- /dev/null +++ b/sim-data/t3_scan_results.jsonl @@ -0,0 +1,10 @@ +{"config": "P1_magma1", "n": 100, "seed0": 73000, "readings": {"normal": {"n": 100, "L1": [50, 100, 50.0], "L2": [9, 50, 18.0], "L3": [0, 9, 0.0]}, "perfect": {"n": 100, "L1": [89, 100, 89.0], "L2": [26, 89, 29.2], "L3": [1, 26, 3.8]}}} +{"config": "P3_drider100", "n": 100, "seed0": 73000, "readings": {"normal": {"n": 100, "L1": [50, 100, 50.0], "L2": [4, 50, 8.0], "L3": [0, 4, 0.0]}, "perfect": {"n": 100, "L1": [89, 100, 89.0], "L2": [19, 89, 21.3], "L3": [0, 19, 0.0]}}} +{"config": "P4_thorn2", "n": 100, "seed0": 73000, "readings": {"normal": {"n": 100, "L1": [48, 100, 48.0], "L2": [2, 48, 4.2], "L3": [0, 2, 0.0]}, "perfect": {"n": 100, "L1": [82, 100, 82.0], "L2": [12, 82, 14.6], "L3": [0, 12, 0.0]}}} +{"config": "P5_chief86", "n": 100, "seed0": 73000, "readings": {"normal": {"n": 100, "L1": [44, 100, 44.0], "L2": [2, 44, 4.5], "L3": [0, 2, 0.0]}, "perfect": {"n": 100, "L1": [88, 100, 88.0], "L2": [10, 88, 11.4], "L3": [0, 10, 0.0]}}} +{"config": "P6_L1swarm", "n": 100, "seed0": 73000, "readings": {"normal": null, "perfect": null}} +{"config": "P2_plb0", "n": 100, "seed0": 73000, "readings": {"normal": {"n": 100, "L1": [50, 100, 50.0], "L2": [9, 50, 18.0], "L3": [0, 9, 0.0]}, "perfect": {"n": 100, "L1": [89, 100, 89.0], "L2": [28, 89, 31.5], "L3": [6, 28, 21.4]}}} +{"config": "P1_magma1+P2_plb0", "n": 100, "seed0": 73000, "readings": {"normal": {"n": 100, "L1": [50, 100, 50.0], "L2": [11, 50, 22.0], "L3": [0, 11, 0.0]}, "perfect": {"n": 100, "L1": [89, 100, 89.0], "L2": [39, 89, 43.8], "L3": [8, 39, 20.5]}}} +{"config": "P1_magma1+P2_plb0+P4_thorn2+P5_chief86", "n": 100, "seed0": 73000, "readings": {"normal": {"n": 100, "L1": [41, 100, 41.0], "L2": [9, 41, 22.0], "L3": [0, 9, 0.0]}, "perfect": {"n": 100, "L1": [81, 100, 81.0], "L2": [40, 81, 49.4], "L3": [9, 40, 22.5]}}} +{"config": "B0_none", "n": 100, "seed0": 73000, "readings": {"normal": {"n": 100, "L1": [50, 100, 50.0], "L2": [2, 50, 4.0], "L3": [0, 2, 0.0]}, "perfect": {"n": 100, "L1": [89, 100, 89.0], "L2": [14, 89, 15.7], "L3": [0, 14, 0.0]}}} +{"config": "P1_magma1+P2_plb0+P3_drider100+P7_percent4", "n": 100, "seed0": 73000, "readings": {"normal": {"n": 100, "L1": [50, 100, 50.0], "L2": [12, 50, 24.0], "L3": [2, 12, 16.7]}, "perfect": {"n": 100, "L1": [89, 100, 89.0], "L2": [50, 89, 56.2], "L3": [17, 50, 34.0]}}} diff --git a/tools/t3_lever_scan.py b/tools/t3_lever_scan.py new file mode 100644 index 00000000..06a7ae99 --- /dev/null +++ b/tools/t3_lever_scan.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""T3 杠杆网格扫描驱动(M-3 迭代 5)——临时批内工具,批末评估去留。 + +用法:python tools/t3_lever_scan.py [--n 100] [--seed0 73000] +流程:对 worktree 数据表打补丁 → 跑 adventure_monte_carlo.gd(N/策略,unique 逐局独立种子) +→ stdout RUNSUM 解析成逐关条件通关率 → git checkout 还原被改表(无论成败)。 +补丁全部按锚文本定位(display_name/variant_id 上下文),打不上即 FAIL 不静默。 +""" +import json +import re +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +GODOT = r"D:\code\godot\Godot_v4.7.2-stable_win64_console.exe" +VAR = ROOT / "game" / "data" / "tables" / "variation.tres" +ENEMY = ROOT / "game" / "data" / "tables" / "enemy.tres" + +# 补丁集:config 名 → [(文件, 锚正则, 替换模板, 说明)] +PATCHES = { + "P1_magma1": [(VAR, r'("variant_id": "magma"[\s\S]{0,200}?"effect_value": )2', r'\g<1>1', "岩浆 2→1")], + "P2_plb0": [(VAR, r"(per_level_bonus = )2", r"\g<1>0", "per_level_bonus 2→0")], + "P3_drider100": [(ENEMY, r'("enemy_id": "drider"[\s\S]{0,400}?"hp": )130', r'\g<1>100', "L2 Boss 卓尔 130→100")], + "P4_thorn2": [(VAR, r'("variant_id": "thorn"[\s\S]{0,200}?"effect_value": )1', r'\g<1>2', "荆棘 1→2")], + "P5_chief86": [(ENEMY, r'("enemy_id": "ogre_chief"[\s\S]{0,400}?"hp": )72', r'\g<1>86', "L1 Boss 酋长 72→86")], + "P7_percent4": [(VAR, r"(percent_of_cells = )5", r"\g<1>4", "percent_of_cells 5→4")], + "P6_L1swarm": [ + (ENEMY, r'("display_name": "地精一伙"[\s\S]{0,200}?"unit_count_max": )3', r'\g<1>4', "L1 d1 一伙 max 3→4"), + (ENEMY, r'("display_name": "大地精"[\s\S]{0,200}?"unit_count_max": )3', r'\g<1>4', "L1 d3 大地精 max 3→4"), + ], +} + + +def apply_patches(name): + if name == "B0_none": + print("[scan] 补丁生效:B0_none(零补丁配对基线)") + return True + edits = PATCHES.get(name) + if edits is None: + # 组合:P1+P2+P3 形如 "C_magma+plb+boss" + parts = [p for p in name.split("+") if p in PATCHES] + if not parts: + print(f"[scan] 未知 config:{name}") + return False + edits = [e for p in parts for e in PATCHES[p]] + touched = {} + try: + for path, pat, rep, desc in edits: + txt = path.read_text(encoding="utf-8") + new, n = re.subn(pat, rep, txt) + if n != 1: + print(f"[scan] FAIL 补丁锚不唯一(命中 {n}):{desc}") + return False + if path not in touched: + touched[path] = txt + path.write_text(new, encoding="utf-8") + print(f"[scan] 补丁生效:{desc}") + return True + finally: + pass # 还原统一由 restore() 做——调用方跑完再调 + + +def restore(): + for f in (VAR, ENEMY): + subprocess.run(["git", "checkout", "--", str(f.relative_to(ROOT))], cwd=ROOT, + capture_output=True, text=True) + print("[scan] 表已还原(git checkout)") + + +def run_mc(n, seed0): + cmd = [GODOT, "--headless", "--path", str(ROOT / "game"), "--script", + "res://smoke/adventure_monte_carlo.gd", "--", str(n), str(seed0), "both", "min"] + proc = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", + errors="replace", timeout=3600) + rows = {"normal": [], "perfect": []} + for line in (proc.stdout or "").splitlines(): + if line.startswith("RUNSUM "): + r = json.loads(line[7:]) + rows[r["policy"]].append(r) + return rows + + +def readings(rows): + out = {} + for pol, rs in rows.items(): + n = len(rs) + if n == 0: + out[pol] = None + continue + d = {"n": n} + for k in (1, 2, 3): + reached = sum(1 for r in rs if r["levels_cleared"] >= k - 1) + passed = sum(1 for r in rs if r["levels_cleared"] >= k) + d[f"L{k}"] = (passed, reached, round(100 * passed / reached, 1) if reached else None) + out[pol] = d + return out + + +def main(): + args = sys.argv[1:] + if not args: + print(__doc__) + return 2 + name = args[0] + n = 100 + seed0 = 73000 + for i, a in enumerate(args): + if a == "--n" and i + 1 < len(args): + n = int(args[i + 1]) + if a == "--seed0" and i + 1 < len(args): + seed0 = int(args[i + 1]) + print(f"[scan] === config {name} · N={n}/策略 · seed0={seed0} ===") + if not apply_patches(name): + restore() + return 1 + try: + rows = run_mc(n, seed0) + finally: + restore() + rd = readings(rows) + for pol in ("normal", "perfect"): + d = rd.get(pol) + if not d: + print(f"[scan] {pol}: 无 RUNSUM(采集异常)") + continue + cells = " · ".join( + f"L{k} {d[f'L{k}'][2]}% ({d[f'L{k}'][0]}/{d[f'L{k}'][1]})" for k in (1, 2, 3)) + print(f"[scan] {pol}: {cells}") + print("[scan] RESULTJSON " + json.dumps({"config": name, "n": n, "seed0": seed0, "readings": rd}, + ensure_ascii=False)) + return 0 + + +if __name__ == "__main__": + sys.exit(main())