fix: calculate chart scores with song formulas and hidden note weights
Publish GitHub Pages output / publish (push) Successful in 54s

This commit is contained in:
2026-09-06 21:51:42 +09:00
parent b97a4008bb
commit e688036d11
409 changed files with 7732 additions and 1559 deletions
+49
View File
@@ -0,0 +1,49 @@
# 코로네 Expert 원본 SUS 분모 계산
2026-09-06, 공개 에셋 카탈로그 revision 70에서 `chart_m0341_expert.sus`를 확보했다. SHA-256은 `b6523da612971b3c8816a8fbb5d86d14d569b259aae84387f2818dd31944d969`이며 서비스 채보의 원본 해시와 일치한다.
## 집계
| 종류 | 개수 | 1개 가중치 | 합 |
|---|---:|---:|---:|
| 일반 | 464 | 1000 | 464000 |
| 독립 플릭 | 85 | 1050 | 89250 |
| 홀드 시작 | 109 | 1000 | 109000 |
| 홀드 끝 | 83 | 1000 | 83000 |
| 홀드 끝 플릭 | 26 | 1000 | 26000 |
| 보이는 릴레이 | 40 | 100 | 4000 |
| 홀드 중간 판정 | 151 | 100 | 15100 |
| 비가시 릴레이 | 105 | 100 | 10500 |
| 베지어 제어점 | 0 | 100 | 0 |
| **전체 분모** | | | **800850** |
득점 판정 958개의 합은 790350이다. 비가시 릴레이 105개를 분모에만 더한다. 일반 노트=1 단위로는 **800.85**다.
공개 [SUS 파서](https://github.com/UntitledCharts/sonolus-level-converters/blob/main/sonolus_converters/holodori_sus/loader.py)의 종류 구분과 압축 토큰 규칙을 대조했다. family 3의 kind 4/5를 베지어/비가시 릴레이로 집계한다. `0xNN`은 base62 빈 칸 압축이며 노트가 아니다. 같은 채널이 여러 줄로 나와도 모두 유지한다. 생성되는 홀드 중간 판정은 SUS 헤더의 카테고리별 개수를 사용한다. 따라서 이 도구는 전체 득점 타임라인을 복원하는 파서가 아니다.
## 검증
- 기존 실측 대상 18개 채보 + 코로네 Easy/Normal/Hard = **알려진 분모 21개 전부 일치**.
- 코로네 Hard: 득점 합 495550 + 비가시 48×100 = **500350**.
- 실제 엔진의 기존 일반 PERFECT 실측 19건은 계속 모두 일치한다.
- 이번 Expert 분모는 원본 구조와 검증된 집계법으로 산출한 값이다. 코로네 Expert의 신규 게임 실측값은 아직 없다.
SUS 헤더의 `SCORE_LEVEL`은 26이지만, 현재 공개 마스터 및 서비스 난이도는 **28**이다. 환산식에는 28을 사용한다. 곡별 계수는 5다.
```text
곡·난이도 배율 = 2.3 × (1 + 5/1000 × (28 − 5)) = 2.5645
scoreRatio = 800.85 / 2.5645 = 312.28309612010145
일반 PERFECT 기본점수 = 종합력 / 312.28309612010145
```
## 적용과 재현
신규 값은 외부 연구 메타데이터와 구분하여 `sus-score-formulas.json`에 출처·해시·집계표·검증 대상과 함께 기록했다. 공식 적용 및 채보 수집은 이 값을 함께 읽고, SUS 해시가 달라지면 적용을 거부한다. 현재 서비스 카탈로그 **388개 모두 formula-v1**이며 추정값으로 남아 있는 채보는 없다. 신규 곡의 분모가 자동으로 확보되는 것은 아니며 원본 확보·집계·검증 과정이 필요하다.
```sh
node scripts/calculate-sus-score-formula.mjs data/raw/sus/chart_m0341_expert.sus
node scripts/validate-sus-score-formulas.mjs data/raw/sus
npm run test:sus-score
```
원본 SUS는 `data/raw/sus`에 저장되어 있으며 Git 및 공개 배포에는 포함하지 않는다.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,119 @@
# 곡별 점수 환산식 검증 — 2026-09-06
## 결론
최신 후속 결과: 원본 SUS로 m0341 Expert 분모 800850을 산출하여 적용했다. 현재 **388개 전체가 공식 기반**이다. 기존 분모 21개와의 교차 검증 및 산출 과정은 [코로네 Expert SUS 분석](m0341-expert-sus.md)에 기록했다. 아래 387개 적용 및 누락 관련 설명은 이전 검증 단계의 이력이다.
후속 구현: 387개 채보에 `formula-v1`을 적용했다. `m0341.expert`는 기존 추정값을 유지한다. 아래 분석 수치와 비교표는 적용 전 상태의 기록이며, 검증 스크립트를 현재 상태에서 실행하면 공식 적용 채보의 모델 간 차이는 0으로 표시된다. 채보 수집 시에도 공식이 우선되며, 실측 적용 명령은 공식 기반 값을 덮어쓰지 않는다.
연구 공식에 곡별 계수·난이도와 `perfect_note_weight`를 적용하면 저장된 **13곡, 18개 곡/난이도, 19건의 일반 PERFECT 실측값을 모두 정확히 재현**한다. 실측값에 맞춘 새로운 상수 피팅은 하지 않았다. 기존 공통 상수보다 근거가 강하며, 곡별 환산값을 생성할 공식으로 채택할 수 있다.
단, 서비스의 `notes` 가중치 합을 분모로 그대로 쓰면 안 된다. 비가시 지점을 포함한 환산용 분모는 `scoreFormula.perfectNoteWeight`로 별도 보관한다. 득점 노트 및 스페셜 시각과 이벤트 계산은 변경하지 않았다.
## 자료와 재현 방법
- 연구 문서: https://docs.google.com/document/d/1P1newxmgxS8b-0raHk5vqnbb8X1nXh1xs3d4gYuhUMQ
- 문서가 제공하는 메타데이터: https://airei.dev/metadata/music_meta.json
- 메타데이터는 이번 조회에서 HTTP 200으로 접근 가능했다. 원문 775개 레코드 중 서비스 카탈로그 및 실측표에 필요한 389개 레코드의 관련 필드만 `song-score-formula-reference.json`에 보존했다. 조회 시각·원본 전체 SHA-256도 포함했다.
- 실측 원본: `data/score-calibrations.json`. 이 자료는 곡/난이도, 종합력, 가창 보너스, 일반 PERFECT 점수를 제공한다. 플랫폼·전체 플레이 로그·측정 시각은 기록되어 있지 않다.
- 로컬 HolodoriDB 한국어 원본 `origin/main`: `1d7c7850bf2df34239c440b5b0cc53bbd72e5d5d`.
- `Music.json`의 곡별 계수 및 `MusicDifficulty.json`의 난이도 레벨은 참고 데이터 389개 모두 일치했다. `Setting.json`의 `liveDeckEvaluationCoefficientPermilMultiply`는 2300이다.
실행:
```sh
node scripts/validate-song-score-formula.mjs
```
스크립트는 고정 참고 데이터와 현재 저장소를 읽기만 한다. 실측 점수는 BigInt 유리수 연산으로 마지막 올림까지 검증하며, 기존 `scoreRatio`는 공식 입력으로 사용하지 않는다. 누락된 실측 참고값, 실측 불일치, 카탈로그 난이도 또는 콤보 수 불일치에서는 실패한다. 실측이 없는 카탈로그의 참고 데이터 누락은 별도로 보고한다.
## 검증 공식
`W`는 일반 PERFECT=1000 단위의 `perfect_note_weight`, `k`는 곡별 `liveScoreCoefficientPermil`, `L`은 난이도 레벨이다.
```text
C = 2.3 × (1 + k/1000 × (L − 5))
scoreRatio = (W/1000) / C
일반 PERFECT 점수 = ceil(종합력 / scoreRatio × (1 + 가창 보너스/100))
```
실측 조건은 콤보·스킬 배율이 없는 일반 PERFECT 기본 점수로 해석한다. 기존 실측 검산도 같은 전제를 사용한다.
올림된 점수 S로부터 ratio는 단일 값으로 결정되지 않는다. A=종합력×가창배율이면 가능한 구간은 `[A/S, A/(S−1))`다. 모든 실측에서 연구식의 ratio가 이 구간 안에 들었으며, 같은 채보에 여러 측정이 있으면 모두 충족했다. 기존 보정 ratio와 연구 ratio가 소수점까지 같을 필요는 없다.
## 전체 실측 결과
| 곡 | 난이도 | 실측 점수 | 연구식 |
|---|---|---:|---:|
| Our Promise | Easy | 6,571 | 6,571 |
| Our Promise | Normal | 4,091 | 4,091 |
| Our Promise | Hard | 2,295 | 2,295 |
| Our Promise | Expert | 1,455 | 1,455 |
| PSYCHO | Hard | 2,023 | 2,023 |
| PSYCHO | Expert, 가창 10% | 1,203 | 1,203 |
| PSYCHO | Expert, 가창 8.26% | 1,184 | 1,184 |
| Los! Los! Los! | Hard | 1,633 | 1,633 |
| Los! Los! Los! | Expert | 1,008 | 1,008 |
| Erandekurete Arigatou | Expert | 1,742 | 1,742 |
| Bling-Bang-Bang-Born | Expert | 998 | 998 |
| ALiCE&u | Expert | 980 | 980 |
| Ahoy!! We are Houshou Pirates | Expert | 1,128 | 1,128 |
| Alien Alien | Expert | 761 | 761 |
| Asuenokyokaisen | Expert | 1,014 | 1,014 |
| Boom! Boom! Tropica Vacation | Expert | 937 | 937 |
| UNDEAD | Expert | 1,113 | 1,113 |
| Zenryoku-Jumping! | Expert | 1,671 | 1,671 |
| NAKAMAKA | Expert | 841 | 841 |
## 원인: 점수를 받는 노트와 환산 분모의 차이
아래는 일반 노트=1로 정규화한 값이다.
| 채보 | 현재 notes 가중치 합 | 참고 playable 가중치 (`base_weight`) | 참고 환산 분모 (`perfect_note_weight`) |
|---|---:|---:|---:|
| Our Promise Expert | 632.05 | 632.05 | 633.95 |
| PSYCHO Expert | 766.25 | 763.55 | 786.55 |
| Boom! Boom! Tropica Vacation Expert | 801.75 | 798.55 | 839.95 |
Our Promise는 playable 집계가 같아도 분모가 1.9 크다. 따라서 현재 노트 분류만 정리한다고 해결되지 않는다. 연구 문서는 비가시 릴레이·베지어 지점에도 분모 가중치를 부여한다고 설명하며, 참고 메타데이터의 `perfect_note_weight − base_weight`는 그 설명과 부합한다. 이번에는 SUS 원문을 독립 파싱해 비가시 지점 개수를 재구성하지는 않았다.
PSYCHO 등에는 현재 notes와 참고 playable 집계 사이에도 차이가 있다. 비가시 분모 누락 외에 홀드·릴레이·끝 플릭 분류의 차이가 추가로 존재한다. 이 차이의 개별 노트 원인은 이번 검증에서 확정하지 않았다. 실제 노트 점수 및 전체 곡 점수 정확성은 별도 점검이 필요하다.
예를 들어 Boom! Boom! Tropica Vacation Expert는 종합력 296,408, 가창 0%, k=7, L=27이다.
```text
현재 notes 합을 분모로 사용: ceil(296408 × 2.6542 / 801.75) = 982
참고 환산 분모 사용: ceil(296408 × 2.6542 / 839.95) = 937
실측: 937
```
환산 분모를 늘리기 위해 비가시 노트를 서비스의 득점·콤보 배열에 추가해서는 안 된다. 분모 전용 필드로 보관해야 한다.
## 서비스 전체 범위 비교
현재 카탈로그 388개 중 참고 데이터가 있는 것은 **387개**다. 이들 모두 난이도 레벨 및 로컬 노트 수와 참고 full-combo 수가 일치한다. 같은 노트 수가 같은 노트 분류·가중치를 보장하지는 않는다.
- 실측 보정 채보: 16개. 올림 전 기본 점수의 연구식 대비 평균 절대 차이 0.0432%, 최대 0.0772%. 위 실측 점수는 기존 보정과 연구식 모두 재현한다.
- 공통 상수 추정 채보: 371개. 연구식 대비 평균 절대 차이 **3.9301%**. 절대 차이 5% 초과 114개, 10% 초과 15개.
- KINGWORLD Expert: 현재 기본 점수가 연구식보다 **21.8420% 낮음**.
- Excuse My Rudeness, But Could You Please RIP? Expert: 현재 기본 점수가 연구식보다 **13.9990% 높음**.
- 참고 데이터 누락: **m0341.expert**. 현재 추정 채보 372개 중 하나다.
위 백분율은 `(현재 올림 전 기본 점수 / 연구식 기본 점수 − 1) × 100`이다. 실측하지 않은 채보의 수치는 두 모델 간 차이이며, 실제 게임 오차율로 검증한 값은 아니다. 카탈로그 포함 전체를 집계했으며 공개 시각에 따른 필터는 적용하지 않았다.
## 적용 시 경계와 남은 검증
1. 곡별 계수·난이도·독립적인 PERFECT 분모를 보존하고 공식으로 ratio를 생성한다. 서비스의 `종합력 / scoreRatio` 인터페이스는 유지 가능하다.
2. 실측값은 회귀 검증 기준으로 유지한다. 현재 보정값과 연구 ratio의 작은 차이는 올림된 1노트 점수만으로는 우열을 판별할 수 없다.
3. 참고 데이터가 없는 m0341 Expert는 분모를 별도 확보하거나 기존 추정과 구분하여 유지해야 한다.
4. 실제 총점까지 검증됐다고 표시하지 않는다. 이번 증거는 일반 PERFECT 1노트 19건이며 스킬, 콤보, PC/모바일 차이, 홀드 중간 올림을 검증하지 않았다.
5. 참고 메타데이터 제공자와 연구 문서는 같은 출처 계열이므로, 완전히 독립적인 두 연구의 교차 검증으로 보지 않는다. 독립적인 대조 기준은 저장소에 이미 있던 실측 점수다.
## 실행 확인
- 신규 검증 스크립트: 19/19 정확 일치.
- 기존 `npm run data:validate-score-calibrations`: 18개 보정 항목 통과.
- `npm run check`: 오류 0, 경고 0.
- `npm run build`: 온라인 빌드 확인.
최초 검증 단계에서는 사용자 동작을 변경하지 않았다. 후속 공식 적용은 UPDATES.md에 기록했다. 기존 작업 중인 다른 변경은 보존했다.
+159
View File
@@ -0,0 +1,159 @@
{
"schemaVersion": 1,
"source": {
"kind": "decrypted-sus",
"catalogRevision": 70,
"parser": "scripts/sus-score-weight.mjs"
},
"charts": [
{
"songId": "m0341",
"fullComboNoteCount": 958,
"counts": {
"NORMAL": 464,
"FLICK": 85,
"LONG_START": 109,
"LONG_END": 83,
"LONG_FLICK_END": 26,
"LONG_RELAY": 40,
"LONG_CONTINUE": 151
},
"invisibleRelayCount": 105,
"bezierPointCount": 0,
"baseWeight": 790350,
"perfectNoteWeight": 800850,
"difficulty": "expert",
"susSha256": "b6523da612971b3c8816a8fbb5d86d14d569b259aae84387f2818dd31944d969",
"liveScoreCoefficientPermil": 5,
"difficultyLevel": 28
}
],
"validationCharts": [
{
"songId": "m0010",
"difficulty": "expert",
"susSha256": "3205deef6bee19b03fd745257421e5003cfd066b6464e70205675a6822b4fe58",
"perfectNoteWeight": 638250
},
{
"songId": "m0035",
"difficulty": "expert",
"susSha256": "bf9a6cf50046633e83e4a3a53113f388ea85ee0f332ed627a14671e2c3f75228",
"perfectNoteWeight": 483100
},
{
"songId": "m0040",
"difficulty": "expert",
"susSha256": "b2857491f68e385d437731e07f26be90a1e3ea19be3e43231b819de7daf3bb6c",
"perfectNoteWeight": 709850
},
{
"songId": "m0047",
"difficulty": "expert",
"susSha256": "db36ac771d525f867c155bb62df46a9bcbc33503f4e5a4b884661d02e6f10f43",
"perfectNoteWeight": 731250
},
{
"songId": "m0108",
"difficulty": "expert",
"susSha256": "cfc11a8411f49be4b3d89f386f8f9557bc6efaf8a56e36bc7b4eeae6891b841d",
"perfectNoteWeight": 968500
},
{
"songId": "m0121",
"difficulty": "expert",
"susSha256": "475f5236d936b13dca7898aac181e42ccb18901d23faf923b4363c4e747bbd6b",
"perfectNoteWeight": 786550
},
{
"songId": "m0121",
"difficulty": "hard",
"susSha256": "6d3a096b459ebf3543c8756686795d0e4515a7e4789f853ad2533071a617473c",
"perfectNoteWeight": 457300
},
{
"songId": "m0144",
"difficulty": "expert",
"susSha256": "6771f00103c6624021e462ccd1fac4ba56b5ea3dc10858d6b36ba82ad2d088a3",
"perfectNoteWeight": 1054200
},
{
"songId": "m0157",
"difficulty": "easy",
"susSha256": "0ce41e57bb03b05db9f8b6e2624464fd3adb112e03165630683b3072b1313ead",
"perfectNoteWeight": 126900
},
{
"songId": "m0157",
"difficulty": "expert",
"susSha256": "6584e49b665276de1df1f8ce69027d290b643f74484b4b643269b9663add45cf",
"perfectNoteWeight": 633950
},
{
"songId": "m0157",
"difficulty": "hard",
"susSha256": "2d6a7bff36c9561c7368b35f54033a625c2c208f44b0b72cf5e0786cfb9e19f2",
"perfectNoteWeight": 385500
},
{
"songId": "m0157",
"difficulty": "normal",
"susSha256": "b17cfb6933fbfdc58197ea6024003c6053446c1eafe9f900fd7512034731bfe7",
"perfectNoteWeight": 210000
},
{
"songId": "m0182",
"difficulty": "expert",
"susSha256": "144c7b3be2c2ff51e203ee697371929fa7748e921766b1a6f20ec66f2f9196cd",
"perfectNoteWeight": 972400
},
{
"songId": "m0182",
"difficulty": "hard",
"susSha256": "f73f824ed32165e60476ef56d7a5cfab30aecf21c2e5eab72425b9f30b2403d7",
"perfectNoteWeight": 581150
},
{
"songId": "m0201",
"difficulty": "expert",
"susSha256": "907ae19b907106d488365765f1787bdf0f117b16b3867feeb011d7593bf652f0",
"perfectNoteWeight": 539400
},
{
"songId": "m0309",
"difficulty": "expert",
"susSha256": "348869afc7110b14e2f84f702a787eee32d46dd34e1463961baac431a4623af8",
"perfectNoteWeight": 839950
},
{
"songId": "m0312",
"difficulty": "expert",
"susSha256": "3c57185b1315e440069d86c5f2df7c0679c34e0ef6be409f89ed22aa67d9cca8",
"perfectNoteWeight": 758700
},
{
"songId": "m0319",
"difficulty": "expert",
"susSha256": "d853bf70aeb3ad267df53a8f445788a2b22cd55ff07e6209d39bff0480e7f43b",
"perfectNoteWeight": 906700
},
{
"songId": "m0341",
"difficulty": "easy",
"susSha256": "ea9fbe7b4c4d466dc50cb30410bc2f410cd5d72fa3f4d58db3a1652c0fa0f3f1",
"perfectNoteWeight": 177000
},
{
"songId": "m0341",
"difficulty": "hard",
"susSha256": "139c990b740a2fb42e5a1c18580b02e91e9da16b0f49c8ab34e544a982404820",
"perfectNoteWeight": 500350
},
{
"songId": "m0341",
"difficulty": "normal",
"susSha256": "13c9e219f5d1ab5a47b78460ace319557c236b9b92574c78d8897bb64297f475",
"perfectNoteWeight": 301800
}
]
}