202 lines
9.1 KiB
Markdown
202 lines
9.1 KiB
Markdown
# 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` 自动找谱面 |
|
||
|
||
> 🔎 **不知道 musicId / 定数?** [maimai.lxns.net/songs](https://maimai.lxns.net/songs)
|
||
> 可以在线查询谱面信息(曲名、难度、定数等),查到 `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 个游戏谱面实测一致)。
|