chart_achievement: 正向支持按音符类别给判定数(绝赞单列),算出确切达成率

This commit is contained in:
2026-10-17 17:43:00 +08:00
parent 25ac522391
commit f6e84cf297
3 changed files with 353 additions and 25 deletions
+103
View File
@@ -427,6 +427,109 @@ def solve_forward(th: Theory, judges: Dict[str, int]) -> ForwardResult:
)
# ------------------------------------------------------------------ 正向:按音符类别精确计算
# 达成率是按「音符权重」加权的,只给汇总判定数时只能得到区间;
# 把判定数按权重类别分开给(绝赞单独一列),就能算出确切的数值。
CLASS_ORDER = ("tap", "hold", "slide", "break")
CLASS_LABEL = {
"tap": "Tap/Star/Touch/EX",
"hold": "Hold 系",
"slide": "Slide 系",
"break": "绝赞(Break 系)",
}
CLASS_WEIGHT = {"tap": 500, "hold": 1000, "slide": 1500, "break": 2500}
CLASS_GROUPS = {
"tap": (GROUP_TAP, GROUP_EX),
"hold": (GROUP_HOLD,),
"slide": (GROUP_SLIDE,),
"break": (GROUP_BREAK, GROUP_BREAK_FLAT, GROUP_BREAK_SLIDE, GROUP_EX_BREAK),
}
# 每类里每个判定键的 (失分, 奖励分);绝赞的 Great / Perfect 有多个档位
CLASS_OPTIONS = {
"tap": {"cp": (0, 0), "perfect": (0, 0), "great": (100, 0),
"good": (250, 0), "miss": (500, 0)},
"hold": {"cp": (0, 0), "perfect": (0, 0), "great": (200, 0),
"good": (500, 0), "miss": (1000, 0)},
"slide": {"cp": (0, 0), "perfect": (0, 0), "great": (300, 0),
"good": (750, 0), "miss": (1500, 0)},
"break": {"cp": (0, 100), "perfect": (0, 75), "perfect50": (0, 50),
"great": (500, 40), "great40": (1000, 40), "great50": (1250, 40),
"good": (1500, 30), "miss": (2500, 0)},
}
_JUDGE_OF_KEY = {
"cp": "cp", "perfect": "perfect", "perfect50": "perfect",
"great": "great", "great40": "great", "great50": "great",
"good": "good", "miss": "miss",
}
def class_capacity(th: Theory, cls: str) -> int:
"""这一类音符在谱面里有多少个。"""
return sum(th.counts.get(g, 0) for g in CLASS_GROUPS[cls])
@dataclass
class ExactResult:
theory: Theory
spec: Dict[str, Dict[str, int]] # 实际参与计算的判定数(已补足 CP)
achievement: Fraction
dx: int
loss_total: int
bonus_total: int
breakdown: Dict[str, Tuple[int, int, int]] # 类别 -> (音符数, 失分, 奖励分)
@property
def score(self) -> int:
return self.theory.all_perfect_score - self.loss_total
def solve_exact(th: Theory, spec: Dict[str, Dict[str, int]]) -> ExactResult:
"""按音符类别给判定数,算出**确切**的达成率与 DX 分。
spec: {"tap": {"cp": 250, "great": 2}, "break": {"cp": 4, "great": 1}, ...}
每类没给满的部分按 CP 补足(非绝赞 CP 与 Perfect 同分;绝赞按 CP 计奖励 100)。
"""
filled: Dict[str, Dict[str, int]] = {}
loss_total = 0
bonus_total = 0
dx = 0
breakdown: Dict[str, Tuple[int, int, int]] = {}
for cls in CLASS_ORDER:
cap = class_capacity(th, cls)
given = {k: int(v) for k, v in (spec.get(cls) or {}).items() if int(v)}
for key in given:
if key not in CLASS_OPTIONS[cls]:
raise ValueError("「%s」没有「%s」这个判定" % (CLASS_LABEL[cls], key))
used = sum(given.values())
if used > cap:
raise ValueError("%s 只有 %d 个音符,但给了 %d 个判定"
% (CLASS_LABEL[cls], cap, used))
if cap > used:
given["cp"] = given.get("cp", 0) + (cap - used)
filled[cls] = given
cls_loss = 0
cls_bonus = 0
for key, n in given.items():
dl, db = CLASS_OPTIONS[cls][key]
cls_loss += dl * n
cls_bonus += db * n
dx += DX_PER[_JUDGE_OF_KEY[key]] * n
loss_total += cls_loss
bonus_total += cls_bonus
breakdown[cls] = (cap, cls_loss, cls_bonus)
w = th.all_perfect_score
if w <= 0:
raise ValueError("谱面里没有计分音符")
achievement = Fraction(100) - Fraction(100 * loss_total, w)
if th.break_bonus_score:
achievement += Fraction(bonus_total, th.break_bonus_score)
return ExactResult(
theory=th, spec=filled, achievement=achievement, dx=dx,
loss_total=loss_total, bonus_total=bonus_total, breakdown=breakdown,
)
# ------------------------------------------------------------------ 反向:工具函数
def _judges_for(th: Theory, miss: int, good: int, great: int,
cp: int, perfect: int) -> Dict[str, int]: