mirror of
https://github.com/283375/arcaea-offline.git
synced 2025-07-01 12:16:26 +00:00
refactor!: calculate -> calculators
This commit is contained in:
@ -1,8 +0,0 @@
|
|||||||
from .b30 import calculate_b30, get_b30_calculated_list
|
|
||||||
from .score import (
|
|
||||||
calculate_constants_from_play_rating,
|
|
||||||
calculate_play_rating,
|
|
||||||
calculate_score_modifier,
|
|
||||||
calculate_score_range,
|
|
||||||
calculate_shiny_pure,
|
|
||||||
)
|
|
@ -1,24 +0,0 @@
|
|||||||
from decimal import Decimal
|
|
||||||
from typing import Dict, List
|
|
||||||
|
|
||||||
from ..models.scores import ScoreCalculated
|
|
||||||
|
|
||||||
|
|
||||||
def get_b30_calculated_list(
|
|
||||||
calculated_list: List[ScoreCalculated],
|
|
||||||
) -> List[ScoreCalculated]:
|
|
||||||
best_scores: Dict[str, ScoreCalculated] = {}
|
|
||||||
for calculated in calculated_list:
|
|
||||||
key = f"{calculated.song_id}_{calculated.rating_class}"
|
|
||||||
stored = best_scores.get(key)
|
|
||||||
if stored and stored.score < calculated.score or not stored:
|
|
||||||
best_scores[key] = calculated
|
|
||||||
ret_list = list(best_scores.values())
|
|
||||||
ret_list = sorted(ret_list, key=lambda c: c.potential, reverse=True)[:30]
|
|
||||||
return ret_list
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_b30(calculated_list: List[ScoreCalculated]) -> Decimal:
|
|
||||||
ptt_list = [Decimal(c.potential) for c in get_b30_calculated_list(calculated_list)]
|
|
||||||
sum_ptt_list = sum(ptt_list)
|
|
||||||
return (sum_ptt_list / len(ptt_list)) if sum_ptt_list else Decimal("0.0")
|
|
@ -1,66 +0,0 @@
|
|||||||
from dataclasses import dataclass
|
|
||||||
from decimal import Decimal
|
|
||||||
from math import floor
|
|
||||||
from typing import Tuple, Union
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_score_range(notes: int, pure: int, far: int):
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_score_modifier(score: int) -> Decimal:
|
|
||||||
if score >= 10000000:
|
|
||||||
return Decimal(2)
|
|
||||||
if score >= 9800000:
|
|
||||||
return Decimal(1) + (Decimal(score - 9800000) / 200000)
|
|
||||||
return Decimal(score - 9500000) / 300000
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_play_rating(constant: int, score: int) -> Decimal:
|
|
||||||
score_modifier = calculate_score_modifier(score)
|
|
||||||
return max(Decimal(0), Decimal(constant) / 10 + score_modifier)
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_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)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ConstantsFromPlayRatingResult:
|
|
||||||
# pylint: disable=invalid-name
|
|
||||||
EXPlus: Tuple[Decimal, Decimal]
|
|
||||||
EX: Tuple[Decimal, Decimal]
|
|
||||||
AA: Tuple[Decimal, Decimal]
|
|
||||||
A: Tuple[Decimal, Decimal]
|
|
||||||
B: Tuple[Decimal, Decimal]
|
|
||||||
C: Tuple[Decimal, Decimal]
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_constants_from_play_rating(play_rating: Union[Decimal, str, float, int]):
|
|
||||||
# pylint: disable=no-value-for-parameter
|
|
||||||
|
|
||||||
play_rating = Decimal(play_rating)
|
|
||||||
|
|
||||||
ranges = []
|
|
||||||
for upper_score, lower_score in [
|
|
||||||
(10000000, 9900000),
|
|
||||||
(9899999, 9800000),
|
|
||||||
(9799999, 9500000),
|
|
||||||
(9499999, 9200000),
|
|
||||||
(9199999, 8900000),
|
|
||||||
(8899999, 8600000),
|
|
||||||
]:
|
|
||||||
upper_score_modifier = calculate_score_modifier(upper_score)
|
|
||||||
lower_score_modifier = calculate_score_modifier(lower_score)
|
|
||||||
ranges.append(
|
|
||||||
(play_rating - upper_score_modifier, play_rating - lower_score_modifier)
|
|
||||||
)
|
|
||||||
|
|
||||||
return ConstantsFromPlayRatingResult(*ranges)
|
|
@ -1,174 +0,0 @@
|
|||||||
from decimal import Decimal
|
|
||||||
from typing import Literal, Optional, Union
|
|
||||||
|
|
||||||
|
|
||||||
class PlayResult:
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
AwakenedIlithPartnerBonus = PartnerBonus(step_bonus="6.0")
|
|
||||||
AwakenedEtoPartnerBonus = PartnerBonus(step_bonus="7.0")
|
|
||||||
AwakenedLunaPartnerBonus = PartnerBonus(step_bonus="7.0")
|
|
||||||
|
|
||||||
|
|
||||||
class AwakenedAyuPartnerBonus(PartnerBonus):
|
|
||||||
def __init__(self, step_bonus: Union[Decimal, str, float, int]):
|
|
||||||
super().__init__(step_bonus=step_bonus)
|
|
||||||
|
|
||||||
|
|
||||||
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")
|
|
||||||
|
|
||||||
|
|
||||||
class StepBooster:
|
|
||||||
def final_value(self) -> Decimal:
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class MemoriesStepBooster(StepBooster):
|
|
||||||
def final_value(self) -> Decimal:
|
|
||||||
return Decimal("4.0")
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_step_original(
|
|
||||||
play_result: PlayResult,
|
|
||||||
*,
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_step(
|
|
||||||
play_result: PlayResult,
|
|
||||||
*,
|
|
||||||
partner_bonus: Optional[PartnerBonus] = None,
|
|
||||||
step_booster: Optional[StepBooster] = None,
|
|
||||||
) -> Decimal:
|
|
||||||
result_original = calculate_step_original(
|
|
||||||
play_result, partner_bonus=partner_bonus, step_booster=step_booster
|
|
||||||
)
|
|
||||||
|
|
||||||
return round(result_original, 1)
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_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)
|
|
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")
|
@ -1,22 +0,0 @@
|
|||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
from arcaea_offline.calculate.world_step import (
|
|
||||||
AwakenedAyuPartnerBonus,
|
|
||||||
LegacyMapStepBooster,
|
|
||||||
PlayResult,
|
|
||||||
calculate_step_original,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_world_step():
|
|
||||||
# the result was copied from https://arcaea.fandom.com/wiki/World_Mode_Mechanics#Calculation
|
|
||||||
# CC BY-SA 3.0
|
|
||||||
|
|
||||||
booster = LegacyMapStepBooster(6, 250)
|
|
||||||
partner_bonus = AwakenedAyuPartnerBonus("+3.6")
|
|
||||||
play_result = PlayResult(play_rating=Decimal("11.299"), partner_step=92)
|
|
||||||
result = calculate_step_original(
|
|
||||||
play_result, partner_bonus=partner_bonus, step_booster=booster
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result.quantize(Decimal("0.000")) == Decimal("175.149")
|
|
42
tests/calculators/test_play_result.py
Normal file
42
tests/calculators/test_play_result.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from arcaea_offline.calculators.play_result import PlayResultCalculators
|
||||||
|
|
||||||
|
|
||||||
|
class TestPlayResultCalculators:
|
||||||
|
def test_score_modifier(self):
|
||||||
|
# Results from https://arcaea.fandom.com/wiki/Potential#Score_Modifier
|
||||||
|
|
||||||
|
assert PlayResultCalculators.score_modifier(10000000) == Decimal("2.0")
|
||||||
|
assert PlayResultCalculators.score_modifier(9900000) == Decimal("1.5")
|
||||||
|
assert PlayResultCalculators.score_modifier(9800000) == Decimal("1.0")
|
||||||
|
assert PlayResultCalculators.score_modifier(9500000) == Decimal("0.0")
|
||||||
|
assert PlayResultCalculators.score_modifier(9200000) == Decimal("-1.0")
|
||||||
|
assert PlayResultCalculators.score_modifier(8900000) == Decimal("-2.0")
|
||||||
|
assert PlayResultCalculators.score_modifier(8600000) == Decimal("-3.0")
|
||||||
|
|
||||||
|
assert PlayResultCalculators.score_modifier(0).quantize(
|
||||||
|
Decimal("-0.00")
|
||||||
|
) == Decimal("-31.67")
|
||||||
|
|
||||||
|
pytest.raises(ValueError, PlayResultCalculators.score_modifier, -1)
|
||||||
|
|
||||||
|
pytest.raises(TypeError, PlayResultCalculators.score_modifier, "9800000")
|
||||||
|
pytest.raises(TypeError, PlayResultCalculators.score_modifier, None)
|
||||||
|
pytest.raises(TypeError, PlayResultCalculators.score_modifier, [])
|
||||||
|
|
||||||
|
def test_play_rating(self):
|
||||||
|
assert PlayResultCalculators.play_rating(10002221, 120) == Decimal("14.0")
|
||||||
|
|
||||||
|
assert PlayResultCalculators.play_rating(5500000, 120) == Decimal("0.0")
|
||||||
|
|
||||||
|
pytest.raises(TypeError, PlayResultCalculators.play_rating, "10002221", 120)
|
||||||
|
pytest.raises(TypeError, PlayResultCalculators.play_rating, 10002221, "120")
|
||||||
|
pytest.raises(TypeError, PlayResultCalculators.play_rating, "10002221", "120")
|
||||||
|
|
||||||
|
pytest.raises(TypeError, PlayResultCalculators.play_rating, 10002221, None)
|
||||||
|
|
||||||
|
pytest.raises(ValueError, PlayResultCalculators.play_rating, -1, 120)
|
||||||
|
pytest.raises(ValueError, PlayResultCalculators.play_rating, 10002221, -1)
|
74
tests/calculators/test_world.py
Normal file
74
tests/calculators/test_world.py
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
from decimal import ROUND_FLOOR, Decimal
|
||||||
|
|
||||||
|
from arcaea_offline.calculators.play_result import PlayResultCalculators
|
||||||
|
from arcaea_offline.calculators.world import (
|
||||||
|
LegacyMapStepBooster,
|
||||||
|
PartnerBonus,
|
||||||
|
WorldMainMapCalculators,
|
||||||
|
WorldPlayResult,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestWorldMainMapCalculators:
|
||||||
|
def test_step_fandom(self):
|
||||||
|
# Final result from https://arcaea.fandom.com/wiki/World_Mode_Mechanics#Calculation
|
||||||
|
# CC BY-SA 3.0
|
||||||
|
|
||||||
|
booster = LegacyMapStepBooster(6, 250)
|
||||||
|
partner_bonus = PartnerBonus(step_bonus="+3.6")
|
||||||
|
play_result = WorldPlayResult(play_rating=Decimal("11.299"), partner_step=92)
|
||||||
|
result = WorldMainMapCalculators.step(
|
||||||
|
play_result, partner_bonus=partner_bonus, step_booster=booster
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.quantize(Decimal("0.000")) == Decimal("175.149")
|
||||||
|
|
||||||
|
def test_step(self):
|
||||||
|
# Results from actual play results, Arcaea v5.5.8c
|
||||||
|
|
||||||
|
def _quantize(decimal: Decimal) -> Decimal:
|
||||||
|
return decimal.quantize(Decimal("0.0"), rounding=ROUND_FLOOR)
|
||||||
|
|
||||||
|
# goldenslaughter FTR [9.7], 9906968
|
||||||
|
# 10.7 > 34.2 < 160
|
||||||
|
assert _quantize(
|
||||||
|
WorldMainMapCalculators.step(
|
||||||
|
WorldPlayResult(
|
||||||
|
play_rating=PlayResultCalculators.play_rating(9906968, 97),
|
||||||
|
partner_step=160,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) == Decimal("34.2")
|
||||||
|
|
||||||
|
# Luna Rossa FTR [9.7], 9984569
|
||||||
|
# 10.8 > 34.7 < 160
|
||||||
|
assert _quantize(
|
||||||
|
WorldMainMapCalculators.step(
|
||||||
|
WorldPlayResult(
|
||||||
|
play_rating=PlayResultCalculators.play_rating(9984569, 97),
|
||||||
|
partner_step=160,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) == Decimal("34.7")
|
||||||
|
|
||||||
|
# ultradiaxon-N3 FTR [10.5], 9349575
|
||||||
|
# 10.2 > 32.7 < 160
|
||||||
|
assert _quantize(
|
||||||
|
WorldMainMapCalculators.step(
|
||||||
|
WorldPlayResult(
|
||||||
|
play_rating=PlayResultCalculators.play_rating(9349575, 105),
|
||||||
|
partner_step=160,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) == Decimal("32.7")
|
||||||
|
|
||||||
|
# san skia FTR [8.3], 10001036
|
||||||
|
# 10.3 > 64.2 < 310
|
||||||
|
assert _quantize(
|
||||||
|
WorldMainMapCalculators.step(
|
||||||
|
WorldPlayResult(
|
||||||
|
play_rating=PlayResultCalculators.play_rating(10001036, 83),
|
||||||
|
partner_step=310,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) == Decimal("64.2")
|
Reference in New Issue
Block a user