mirror of
https://github.com/283375/arcaea-offline.git
synced 2025-07-01 20:26:27 +00:00
refactor!: calculate -> calculators
This commit is contained in:
2
src/arcaea_offline/calculators/__init__.py
Normal file
2
src/arcaea_offline/calculators/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from . import world
|
||||
from .play_result import PlayResultCalculators, calculate_constants_from_play_rating
|
105
src/arcaea_offline/calculators/play_result.py
Normal file
105
src/arcaea_offline/calculators/play_result.py
Normal file
@ -0,0 +1,105 @@
|
||||
from decimal import Decimal
|
||||
from math import floor
|
||||
from typing import Tuple, TypedDict, Union
|
||||
|
||||
from arcaea_offline.constants.play_result import ScoreLowerLimits
|
||||
|
||||
|
||||
class PlayResultCalculators:
|
||||
@staticmethod
|
||||
def score_possible_range(notes: int, pure: int, far: int) -> tuple[int, int]:
|
||||
"""
|
||||
Returns the possible range of score based on the given values.
|
||||
|
||||
The first integer of returned tuple is the lower limit of the score,
|
||||
and the second integer is the upper limit.
|
||||
|
||||
For example, ...
|
||||
"""
|
||||
single_note_score = 10000000 / Decimal(notes)
|
||||
|
||||
actual_score = floor(
|
||||
single_note_score * pure + single_note_score * Decimal(0.5) * far
|
||||
)
|
||||
return (actual_score, actual_score + pure)
|
||||
|
||||
@staticmethod
|
||||
def shiny_pure(notes: int, score: int, pure: int, far: int) -> int:
|
||||
single_note_score = 10000000 / Decimal(notes)
|
||||
actual_score = single_note_score * pure + single_note_score * Decimal(0.5) * far
|
||||
return score - floor(actual_score)
|
||||
|
||||
@staticmethod
|
||||
def score_modifier(score: int) -> Decimal:
|
||||
"""
|
||||
Returns the score modifier of the given score
|
||||
|
||||
https://arcaea.fandom.com/wiki/Potential#Score_Modifier
|
||||
|
||||
:param score: The score of the play result, e.g. 9900000
|
||||
:return: The modifier of the given score, e.g. Decimal("1.5")
|
||||
"""
|
||||
if not isinstance(score, int):
|
||||
raise TypeError("score must be an integer")
|
||||
if score < 0:
|
||||
raise ValueError("score cannot be negative")
|
||||
|
||||
if score >= 10000000:
|
||||
return Decimal(2)
|
||||
if score >= 9800000:
|
||||
return Decimal(1) + (Decimal(score - 9800000) / 200000)
|
||||
return Decimal(score - 9500000) / 300000
|
||||
|
||||
@classmethod
|
||||
def play_rating(cls, score: int, constant: int) -> Decimal:
|
||||
"""
|
||||
Returns the play rating of the given score
|
||||
|
||||
https://arcaea.fandom.com/wiki/Potential#Play_Rating
|
||||
|
||||
:param constant: The (constant * 10) of the played chart, e.g. 120 for Testify[BYD]
|
||||
:param score: The score of the play result, e.g. 10002221
|
||||
:return: The play rating of the given values, e.g. Decimal("14.0")
|
||||
"""
|
||||
if not isinstance(score, int):
|
||||
raise TypeError("score must be an integer")
|
||||
if not isinstance(constant, int):
|
||||
raise TypeError("constant must be an integer")
|
||||
if score < 0:
|
||||
raise ValueError("score cannot be negative")
|
||||
if constant < 0:
|
||||
raise ValueError("constant cannot be negative")
|
||||
|
||||
score_modifier = cls.score_modifier(score)
|
||||
return max(Decimal(0), Decimal(constant) / 10 + score_modifier)
|
||||
|
||||
class ConstantsFromPlayRatingResult(TypedDict):
|
||||
EX_PLUS: Tuple[Decimal, Decimal]
|
||||
EX: Tuple[Decimal, Decimal]
|
||||
AA: Tuple[Decimal, Decimal]
|
||||
A: Tuple[Decimal, Decimal]
|
||||
B: Tuple[Decimal, Decimal]
|
||||
C: Tuple[Decimal, Decimal]
|
||||
|
||||
@classmethod
|
||||
def constants_from_play_rating(
|
||||
cls, play_rating: Union[Decimal, str, float, int]
|
||||
) -> ConstantsFromPlayRatingResult:
|
||||
play_rating = Decimal(play_rating)
|
||||
|
||||
def _result(score_upper: int, score_lower: int) -> Tuple[Decimal, Decimal]:
|
||||
upper_score_modifier = cls.score_modifier(score_upper)
|
||||
lower_score_modifier = cls.score_modifier(score_lower)
|
||||
return (
|
||||
play_rating - upper_score_modifier,
|
||||
play_rating - lower_score_modifier,
|
||||
)
|
||||
|
||||
return {
|
||||
"EX_PLUS": _result(10000000, ScoreLowerLimits.EX_PLUS),
|
||||
"EX": _result(ScoreLowerLimits.EX_PLUS - 1, ScoreLowerLimits.EX),
|
||||
"AA": _result(ScoreLowerLimits.EX - 1, ScoreLowerLimits.AA),
|
||||
"A": _result(ScoreLowerLimits.AA - 1, ScoreLowerLimits.A),
|
||||
"B": _result(ScoreLowerLimits.A - 1, ScoreLowerLimits.B),
|
||||
"C": _result(ScoreLowerLimits.B - 1, ScoreLowerLimits.C),
|
||||
}
|
11
src/arcaea_offline/calculators/world/__init__.py
Normal file
11
src/arcaea_offline/calculators/world/__init__.py
Normal file
@ -0,0 +1,11 @@
|
||||
from ._common import MemoriesStepBooster, PartnerBonus, WorldPlayResult
|
||||
from .legacy import LegacyMapStepBooster
|
||||
from .main import WorldMainMapCalculators
|
||||
from .partners import (
|
||||
AmaneBelowExPartnerBonus,
|
||||
AwakenedEtoPartnerBonus,
|
||||
AwakenedIlithPartnerBonus,
|
||||
AwakenedLunaPartnerBonus,
|
||||
MayaPartnerBonus,
|
||||
MithraTerceraPartnerBonus,
|
||||
)
|
50
src/arcaea_offline/calculators/world/_common.py
Normal file
50
src/arcaea_offline/calculators/world/_common.py
Normal file
@ -0,0 +1,50 @@
|
||||
from decimal import Decimal
|
||||
from typing import Union
|
||||
|
||||
|
||||
class WorldPlayResult:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
play_rating: Union[Decimal, str, float, int],
|
||||
partner_step: Union[Decimal, str, float, int],
|
||||
):
|
||||
self.__play_rating = play_rating
|
||||
self.__partner_step = partner_step
|
||||
|
||||
@property
|
||||
def play_rating(self):
|
||||
return Decimal(self.__play_rating)
|
||||
|
||||
@property
|
||||
def partner_step(self):
|
||||
return Decimal(self.__partner_step)
|
||||
|
||||
|
||||
class PartnerBonus:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
step_bonus: Union[Decimal, str, float, int] = Decimal("0.0"),
|
||||
final_multiplier: Union[Decimal, str, float, int] = Decimal("1.0"),
|
||||
):
|
||||
self.__step_bonus = step_bonus
|
||||
self.__final_multiplier = final_multiplier
|
||||
|
||||
@property
|
||||
def step_bonus(self):
|
||||
return Decimal(self.__step_bonus)
|
||||
|
||||
@property
|
||||
def final_multiplier(self):
|
||||
return Decimal(self.__final_multiplier)
|
||||
|
||||
|
||||
class StepBooster:
|
||||
def final_value(self) -> Decimal:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class MemoriesStepBooster(StepBooster):
|
||||
def final_value(self) -> Decimal:
|
||||
return Decimal("4.0")
|
45
src/arcaea_offline/calculators/world/legacy.py
Normal file
45
src/arcaea_offline/calculators/world/legacy.py
Normal file
@ -0,0 +1,45 @@
|
||||
from decimal import Decimal
|
||||
from typing import Literal
|
||||
|
||||
from ._common import StepBooster
|
||||
|
||||
|
||||
class LegacyMapStepBooster(StepBooster):
|
||||
def __init__(
|
||||
self,
|
||||
stamina: Literal[2, 4, 6],
|
||||
fragments: Literal[100, 250, 500, None],
|
||||
):
|
||||
self.stamina = stamina
|
||||
self.fragments = fragments
|
||||
|
||||
@property
|
||||
def stamina(self):
|
||||
return self.__stamina
|
||||
|
||||
@stamina.setter
|
||||
def stamina(self, value: Literal[2, 4, 6]):
|
||||
if value not in [2, 4, 6]:
|
||||
raise ValueError("stamina can only be one of [2, 4, 6]")
|
||||
self.__stamina = value
|
||||
|
||||
@property
|
||||
def fragments(self):
|
||||
return self.__fragments
|
||||
|
||||
@fragments.setter
|
||||
def fragments(self, value: Literal[100, 250, 500, None]):
|
||||
if value not in [100, 250, 500, None]:
|
||||
raise ValueError("fragments can only be one of [100, 250, 500, None]")
|
||||
self.__fragments = value
|
||||
|
||||
def final_value(self) -> Decimal:
|
||||
stamina_multiplier = Decimal(self.stamina)
|
||||
fragments_multiplier = Decimal(1)
|
||||
if self.fragments == 100:
|
||||
fragments_multiplier = Decimal("1.1")
|
||||
elif self.fragments == 250:
|
||||
fragments_multiplier = Decimal("1.25")
|
||||
elif self.fragments == 500:
|
||||
fragments_multiplier = Decimal("1.5")
|
||||
return stamina_multiplier * fragments_multiplier
|
57
src/arcaea_offline/calculators/world/main.py
Normal file
57
src/arcaea_offline/calculators/world/main.py
Normal file
@ -0,0 +1,57 @@
|
||||
from decimal import Decimal
|
||||
from typing import Optional, Union
|
||||
|
||||
from ._common import PartnerBonus, StepBooster, WorldPlayResult
|
||||
|
||||
|
||||
class WorldMainMapCalculators:
|
||||
@staticmethod
|
||||
def step(
|
||||
play_result: WorldPlayResult,
|
||||
*,
|
||||
partner_bonus: Optional[PartnerBonus] = None,
|
||||
step_booster: Optional[StepBooster] = None,
|
||||
) -> Decimal:
|
||||
ptt = play_result.play_rating
|
||||
step = play_result.partner_step
|
||||
if partner_bonus:
|
||||
partner_bonus_step = partner_bonus.step_bonus
|
||||
partner_bonus_multiplier = partner_bonus.final_multiplier
|
||||
else:
|
||||
partner_bonus_step = Decimal("0")
|
||||
partner_bonus_multiplier = Decimal("1.0")
|
||||
|
||||
result = (Decimal("2.45") * ptt.sqrt() + Decimal("2.5")) * (step / 50)
|
||||
result += partner_bonus_step
|
||||
result *= partner_bonus_multiplier
|
||||
if step_booster:
|
||||
result *= step_booster.final_value()
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def play_rating_from_step(
|
||||
step: Union[Decimal, str, int, float],
|
||||
partner_step_value: Union[Decimal, str, int, float],
|
||||
*,
|
||||
partner_bonus: Optional[PartnerBonus] = None,
|
||||
step_booster: Optional[StepBooster] = None,
|
||||
):
|
||||
step = Decimal(step)
|
||||
partner_step_value = Decimal(partner_step_value)
|
||||
|
||||
# get original play result
|
||||
if partner_bonus and partner_bonus.final_multiplier:
|
||||
step /= partner_bonus.final_multiplier
|
||||
if step_booster:
|
||||
step /= step_booster.final_value()
|
||||
|
||||
if partner_bonus and partner_bonus.step_bonus:
|
||||
step -= partner_bonus.step_bonus
|
||||
|
||||
play_rating_sqrt = (
|
||||
Decimal(50) * step - Decimal("2.5") * partner_step_value
|
||||
) / (Decimal("2.45") * partner_step_value)
|
||||
return (
|
||||
play_rating_sqrt**2 if play_rating_sqrt >= 0 else -(play_rating_sqrt**2)
|
||||
)
|
16
src/arcaea_offline/calculators/world/partners.py
Normal file
16
src/arcaea_offline/calculators/world/partners.py
Normal file
@ -0,0 +1,16 @@
|
||||
from ._common import PartnerBonus
|
||||
|
||||
AwakenedIlithPartnerBonus = PartnerBonus(step_bonus="6.0")
|
||||
AwakenedEtoPartnerBonus = PartnerBonus(step_bonus="7.0")
|
||||
AwakenedLunaPartnerBonus = PartnerBonus(step_bonus="7.0")
|
||||
|
||||
|
||||
AmaneBelowExPartnerBonus = PartnerBonus(final_multiplier="0.5")
|
||||
|
||||
|
||||
class MithraTerceraPartnerBonus(PartnerBonus):
|
||||
def __init__(self, step_bonus: int):
|
||||
super().__init__(step_bonus=step_bonus)
|
||||
|
||||
|
||||
MayaPartnerBonus = PartnerBonus(final_multiplier="2.0")
|
Reference in New Issue
Block a user