Files
Astral/tools/chart_achievement/README.md
T

199 lines
8.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# chart_achievement —— 舞萌DX 谱面达成率 / DX 分计算器
给定谱面(`.ma2`)和判定数,算出**达成率**与 **DX 分**;反过来,给定目标达成率,
倒算出所有能打出该达成率的判定组合。
纯本地、纯标准库(不需要联网、不写任何游戏文件)。
## 怎么跑
```powershell
cd tools/chart_achievement
python chart_achievement.py --help
python chart_achievement.py # 不给参数 = 交互向导
```
## 谱面从哪来(二选一)
| 参数 | 说明 |
| --- | --- |
| `--chart <路径>` | 直接给 `.ma2` 文件,如 `.../music/music000008/000008_03.ma2` |
| `--game <目录> --music-id <ID>` | 给游戏目录(`Sinmai_Data`、`StreamingAssets`、`A000`、`music` 都能认),按 `musicid` 自动找谱面 |
### 难度(--level)
难度用**游戏里规定的数字**表示,也就是谱面文件名后缀 `_00` ~ `_04` 的那个数字
(对应游戏主数据 `musicDifficulty/MusicDifficulty00000X` 的 `<id>`):
| 数字 | 难度 | 谱面文件名 |
| --- | --- | --- |
| 0 | BASIC | `<id>_00.ma2` |
| 1 | ADVANCED | `<id>_01.ma2` |
| 2 | EXPERT | `<id>_02.ma2` |
| 3 | MASTER | `<id>_03.ma2` |
| 4 | RE:MASTER | `<id>_04.ma2` |
`--level 0` ~ `--level 4` 或 `--level BASIC` / `ADVANCED` / `EXPERT` / `MASTER` / `RE:MASTER`
(大小写随意;`--difficulty` 是同一个参数的别名)都行,**默认 3(MASTER)**;
不给时会在 stderr 提示用了默认值。
注意:这里的「等级」指**谱面难度**。谱面的数值等级(如 `12+`)是另一回事,
由 `Music.xml` 自动读出,只用于显示。
## 正向:已知判定数 → 达成率 + DX 分
达成率是**按音符权重加权**的(Tap 500 / Hold 1000 / Slide 1500 / Break 2500),
所以有两种给判定数的方式:
### ① 精确:按音符类别给(推荐,得到确切数值)
```powershell
python chart_achievement.py --game "<...>\Sinmai_Data" --music-id 8 --level 3 --tap 260/0/3 --break 6 # 3 = MASTER
```
每个类别写 `CP/Perfect/Great/Good/Miss` 五个数(`/`、`,` 或空格分隔,后面可以省略),
**没给满的部分按 CP 算**:
| 参数 | 类别 | 权重 |
| --- | --- | --- |
| `--tap` | Tap / Star / Touch / EX | 500 |
| `--hold` | Hold 系 | 1000 |
| `--slide` | Slide 系 | 1500 |
| `--break` | 绝赞(Break 系) | 2500 |
绝赞的 Great / Perfect 本身还有档位,需要时可以再细分(不给就按 Great 失分 20%、Perfect 奖励 75 算):
| 参数 | 含义 |
| --- | --- |
| `--break-great40 N` | 绝赞里 N 个 Great 是「失分 40%」档(Great2nd) |
| `--break-great50 N` | 绝赞里 N 个 Great 是「失分 50%」档(Great3rd) |
| `--break-perfect50 N` | 绝赞里 N 个 Perfect 是「奖励分 50」档(Perfect2nd) |
输出就是一个**确切**的达成率,并附上失分 / 绝赞奖励分的明细:
```
判定输入(按音符类别;Break = 绝赞;没给的部分按 CP 算)
Tap/Star/Touch/EX (500) 263 个:CP 260 · Great 3
Hold (1000) 14 个:CP 14
Slide (1500) 19 个:CP 19
Break (2500) 6 个:CP 6
结果
达成率 100.8413% (确切值)
DX 分 900 / 906 (99.34%)
```
### ② 汇总:只给总判定数(得到估计值 + 区间)
```powershell
python chart_achievement.py --chart "<...>\000008_03.ma2" --cp 290 --perfect 8 --great 3 --good 1
```
`--cp` / `--perfect` / `--great` / `--good` 都给总数,`--miss` 留空时自动补足
(`miss = 总音符数 - CP - Perfect - Great - Good`)。
这时达成率取决于这些 Great/Good/Miss **落在哪类音符上**(落在绝赞上扣得多、落在 Tap 上扣得少),
所以给出的是「按类别比例分摊的估计值」+ 一个区间(下界 = 扣分都落在高权重音符上,
上界 = 都落在低权重音符上)。想要一个确切数值就用上面的 ①。
DX 分在两种方式下都是确定的:`3×CP + 2×Perfect + 1×Great`。
## 反向:已知达成率 → 判定组合 + DX 分
```powershell
python chart_achievement.py --game "D:\SDGB2026\Package\Sinmai_Data" --music-id 8 --level 3 --achievement 100.75 # 3 = MASTER
python chart_achievement.py --game "D:\SDGB2026\Package\Sinmai_Data" --music-id 8 --level 2 --achievement 99 --mode range # 2 = EXPERT
```
两种模式(`--mode`):
| 模式 | 含义 | 什么时候用 |
| --- | --- | --- |
| `exact`(默认) | 只列**真的能打出**该达成率的 (Miss, Good, Great) 组合,每组都做过严格验证 | 数据可信、想知道精确答案 |
| `range` | 列达成率**区间覆盖**该值的组合(更快、更宽松) | 判定数只是估算 / 想快速看个大概 |
`--limit N` 限制最多输出多少组(默认 200)。`--json` 输出 JSON。
输出里的 `CP+P` 是「Critical Perfect + Perfect 的总数」;`CP 范围` 是其中
CP 的物理可能范围(Perfect 只能落在绝赞音符上,所以 CP 不能少于
`CP+P - 绝赞音符数`)。CP 越多 DX 分越高,所以 DX 分也是这个范围。
### 精确模式的限制
精确验证用一个「坏音符背包 DP」,代价随坏音符个数增长,所以:
- 只严格验证 `Miss+Good+Great ≤ 12` 的组合;
- 坏音符更多的组合不验证(输出里会提示「坏音符超过 12 个的候选组合没有验证」);
- 若候选组合全都超过 12 个坏音符(例如 1000+ 绝赞的异常谱面),会自动退回区间模式并说明原因;
- DP 有 8 秒时间预算,超时也退回区间模式。
## 计分模型(与游戏一致)
达成率按**音符权重**加权,不是按个数平均:
```
达成率 = 100 × Σ(单音符得分) / 理论分W + Σ(绝赞奖励分) / 绝赞理论奖励分B
```
- 理论分 W = Σ 每个计分音符的权重(Tap/Star/Touch 500、Hold 系 1000、Slide 系 1500、Break 系 2500)
- 绝赞理论奖励分 B = 100 × 绝赞音符个数(Break / Break(长按判定) / BreakSlide / EXBreak)
- 满分 = **101.0000%**(谱面有绝赞)/ **100.0000%**(无绝赞)
- 接续滑条节点(`CNSI_` 等 ConnectSlide)**不计分、不计入总数**
- DX 分 = `3×CP + 2×Perfect + 1×Great`(与音符类型无关)
### 各类音符的失分 / 奖励分
| 音符组(权重) | CP | Perfect | Great | Good | Miss |
| --- | --- | --- | --- | --- | --- |
| Tap / Star / Touch(500) | 0 | 0 | 100 | 250 | 500 |
| Hold 系(1000) | 0 | 0 | 200 | 500 | 1000 |
| Slide 系(1500) | 0 | 0 | 300 | 750 | 1500 |
| Break(2500,分档) | 0 | 0 | 500 / 1000 / 1250 | 1500 | 2500 |
| Break 长按判定(2500) | 0 | 0 | 500 | 1500 | 2500 |
| BreakSlide(2500,分档) | 0 | 0 | 500 / 1000 / 1250 | 1500 | 2500 |
| EX(500) | 0 | — | — | — | 500 |
| EXBreak(2500) | 0 | — | — | — | 2500 |
(表里是「失分」,即 `权重 - 得分`;CP/Perfect 的失分都是 0,所以
**除绝赞外,Critical Perfect 与 Perfect 加的达成率完全一样**。)
绝赞音符额外有奖励分:
| 判定 | 奖励分 |
| --- | --- |
| Critical Perfect | 100 |
| Perfect | 75(FastPerfect/LatePerfect)/ 50(Perfect2nd) |
| Great | 40 |
| Good | 30 |
| Miss | 0 |
这就是绝赞必须区分 CP / Perfect 的原因:普通分一样,但奖励分不同
(CP 100、Perfect 75 或 50)。
### 音符组的划分依据(反编译源码)
- 走长按结算的 Break(`BRHLD`)判定档位更少(Great 只有一档、Perfect 只有 75 档),所以单独成组。
- 滑条系(`BRSI_`/`BRSCL`…)走 SlideOut 判定表,只有 Great 三档、没有 Perfect 档。
- EX 音符(`XTP`/`XST`/`BXTAP`/`BXSTR`,即 ExTap/ExStar/ExBreakTap/ExBreakStar)
走 ExTap 判定表,非 Critical 的窗口宽度为 0,所以**只能判 Critical 或 Miss**。
- `XHO`/`BXHLD`(ExHold/ExBreakHold)头部用 ExTap 表但结果还要过长按结算,
等价于普通 Hold / Break(长按判定)。
## 文件
| 文件 | 内容 |
| --- | --- |
| `ma2.py` | `.ma2` 解析、指令→音符组映射、`Music.xml` 读取、路径拼接 |
| `score.py` | 计分模型、正向求解、反向求解(区间 / 精确) |
| `chart_achievement.py` | 命令行界面 / 交互向导 |
## 已知局限
- 只按谱面文本计算,不涉及游戏运行时的实际判定时机分布。绝赞的 Great 档位(失分 20% / 40% / 50%)
和 Perfect 档位(奖励 75 / 50)需要自己用 `--break-great40` / `--break-great50` /
`--break-perfect50` 指定,不给就按 20% / 75 算。
- 汇总模式(只给总判定数)只能给区间——这不是精度问题,是信息量不够:同样的判定总数,
落在绝赞上和落在 Tap 上扣的分不同。要确切值就按类别给(`--tap` / `--hold` / `--slide` / `--break`)。
- 精确反向只验证坏音符数 ≤ 12 的组合(见上文)。
- 若 `.ma2` 里的 `T_NUM_ALL` 与解析出的计分音符数不一致,会在输出里给警告
(全部 5419 个游戏谱面实测一致)。