From bfc86df2e8a0511620b0e7a6f6a097bb87c9e03e Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Tue, 27 Feb 2024 23:05:51 +0000 Subject: [PATCH] add code --- src/star_rail_relic_trimmer/__init__.py | 0 src/star_rail_relic_trimmer/load.py | 191 ++++++++++++++++++++++++ src/star_rail_relic_trimmer/models.py | 135 +++++++++++++++++ src/star_rail_relic_trimmer/optimise.py | 63 ++++++++ 4 files changed, 389 insertions(+) create mode 100644 src/star_rail_relic_trimmer/__init__.py create mode 100644 src/star_rail_relic_trimmer/load.py create mode 100644 src/star_rail_relic_trimmer/models.py create mode 100644 src/star_rail_relic_trimmer/optimise.py diff --git a/src/star_rail_relic_trimmer/__init__.py b/src/star_rail_relic_trimmer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/star_rail_relic_trimmer/load.py b/src/star_rail_relic_trimmer/load.py new file mode 100644 index 0000000..4485dda --- /dev/null +++ b/src/star_rail_relic_trimmer/load.py @@ -0,0 +1,191 @@ +from __future__ import annotations + +import itertools +from enum import Enum +from pathlib import Path +from typing import Any, Generator + +import yaml +from pydantic import BaseModel +from rich.console import Console +from rich.table import Table + +from star_rail_relic_trimmer.models import CharacterRelic, Position, RelicToKeep +from star_rail_relic_trimmer.optimise import main_stat_plus_stat, no_main_stat_two_stats, prune_any + + +def convert_model_to_dict( + obj: BaseModel | list[BaseModel] | dict[str, BaseModel], +) -> dict[str, Any] | list[dict[str, Any]]: + if isinstance(obj, BaseModel): + return {k: (convert_model_to_dict(v) if v is not None else None) for k, v in obj.model_dump().items()} + + if isinstance(obj, Enum): + return obj.value + + if isinstance(obj, list): + return [convert_model_to_dict(item) for item in obj] + + if isinstance(obj, dict): + return {k: convert_model_to_dict(v) for k, v in obj.items()} + + return obj + + +def deduplicate_relics(relics: list[RelicToKeep]) -> set[RelicToKeep]: + unique_relics = {} + for relic in relics: + key = (relic.primary_stat, relic.secondary_stat, relic.tertiary_stat) + + if key not in unique_relics: + unique_relics[key] = relic + + else: + unique_relics[key].characters.update(relic.characters) + + return set(unique_relics.values()) + + +def sort_relics(relics_to_keep: Generator[RelicToKeep, Any, None]): + return sorted( + convert_model_to_dict(list(deduplicate_relics(relics_to_keep))), + key=lambda x: (x["primary_stat"] == "ANY", x["primary_stat"]), + ) + + +def show_relics_to_keep(data_file: Path): + with data_file.open() as f: + data = yaml.safe_load(f) + + character_relics = [CharacterRelic(**relic) for relic in data["Relics"]] + + head_relics = [] + hand_relics = [] + body_relics = [] + feet_relics = [] + sphere_relics = [] + rope_relics = [] + + for character_relic in character_relics: + head_relics.extend( + itertools.chain( + main_stat_plus_stat( + relic=character_relic.Head, + stats=character_relic.stats, + character_name=character_relic.CharacterName, + position=Position.Head, + ), + no_main_stat_two_stats( + stats=character_relic.stats, + character_name=character_relic.CharacterName, + position=Position.Head, + ), + ), + ) + hand_relics.extend( + itertools.chain( + main_stat_plus_stat( + relic=character_relic.Hands, + stats=character_relic.stats, + character_name=character_relic.CharacterName, + position=Position.Hands, + ), + no_main_stat_two_stats( + stats=character_relic.stats, + character_name=character_relic.CharacterName, + position=Position.Hands, + ), + ), + ) + body_relics.extend( + itertools.chain( + main_stat_plus_stat( + relic=character_relic.Body, + stats=character_relic.stats, + character_name=character_relic.CharacterName, + position=Position.Body, + ), + no_main_stat_two_stats( + stats=character_relic.stats, + character_name=character_relic.CharacterName, + position=Position.Body, + ), + ), + ) + feet_relics.extend( + itertools.chain( + main_stat_plus_stat( + relic=character_relic.Feet, + stats=character_relic.stats, + character_name=character_relic.CharacterName, + position=Position.Feet, + ), + no_main_stat_two_stats( + stats=character_relic.stats, + character_name=character_relic.CharacterName, + position=Position.Feet, + ), + ), + ) + sphere_relics.extend( + itertools.chain( + main_stat_plus_stat( + relic=character_relic.Sphere, + stats=character_relic.stats, + character_name=character_relic.CharacterName, + position=Position.Sphere, + ), + no_main_stat_two_stats( + stats=character_relic.stats, + character_name=character_relic.CharacterName, + position=Position.Sphere, + ), + ), + ) + rope_relics.extend( + itertools.chain( + main_stat_plus_stat( + relic=character_relic.Rope, + stats=character_relic.stats, + character_name=character_relic.CharacterName, + position=Position.Rope, + ), + no_main_stat_two_stats( + stats=character_relic.stats, + character_name=character_relic.CharacterName, + position=Position.Rope, + ), + ), + ) + + head_relics_sorted = sort_relics(prune_any(head_relics)) + hand_relics_sorted = sort_relics(prune_any(hand_relics)) + body_relics_sorted = sort_relics(prune_any(body_relics)) + feet_relics_sorted = sort_relics(prune_any(feet_relics)) + sphere_relics_sorted = sort_relics(prune_any(sphere_relics)) + rope_relics_sorted = sort_relics(prune_any(rope_relics)) + + for _table in ( + ("Head Relics", head_relics_sorted), + ("Hand Relics", hand_relics_sorted), + ("Body Relics", body_relics_sorted), + ("Feet Relics", feet_relics_sorted), + ("Sphere Relics", sphere_relics_sorted), + ("Rope Relics", rope_relics_sorted), + ): + table = Table(title=_table[0]) + table.add_column("Primary Stat", style="white") + table.add_column("Secondary Stat", style="white") + table.add_column("Tertiary Stat", style="white") + table.add_column("Characters", style="white") + + for item in _table[1]: + table.add_row( + item.get("primary_stat"), + item.get("secondary_stat"), + item.get("tertiary_stat"), + ",".join(item.get("characters")), + ) + + console = Console() + console.print(table) diff --git a/src/star_rail_relic_trimmer/models.py b/src/star_rail_relic_trimmer/models.py new file mode 100644 index 0000000..7e5e0df --- /dev/null +++ b/src/star_rail_relic_trimmer/models.py @@ -0,0 +1,135 @@ +from __future__ import annotations + +from enum import Enum + +from pydantic import BaseModel + + +class Stat(Enum): + HP = "HP" + ATK = "ATK" + HP_PERC = "HP%" + ATK_PERC = "ATK%" + DEF_PERC = "DEF%" + EFFECTHITRATE = "EffectHitRate" + OUTGOINGHEALING = "OutgoingHealing" + CRITRATE = "CRITRate" + CRITDMG = "CRITDMG" + SPD = "SPD" + PHYSICALDMG = "PhysicalDMG" + FIREDMG = "FireDMG" + ICEDMG = "IceDMG" + WINDDMG = "WindDMG" + LIGHTNINGDMG = "LightningDMG" + QUANTUMDMG = "QuantumDMG" + IMAGINARYDMG = "ImaginaryDMG" + BREAKEFFECT = "BreakEffect" + ENERGYREGENERATIONRATE = "EnergyRegenerationRate" + EFFECTRES_PERC = "EffectRes%" + ANY = "ANY" + +class Position(Enum): + Head = "Head" + Hands = "Hands" + Body = "Body" + Feet = "Feet" + Sphere = "Sphere" + Rope = "Rope" + + +class CharacterRelic(BaseModel): + CharacterName: str + Head: Head + Hands: Hands + Body: Body + Feet: Feet + Sphere: Sphere + Rope: Rope + stats: list[Stat] + + +class Relic(BaseModel): + stat: Stat + allowed_stats: list[Stat] + + +class Head(Relic): + stat: Stat + allowed_stats: list[Stat] = [Stat.HP] + + +class Hands(Relic): + stat: Stat + allowed_stats: list[Stat] = [Stat.ATK] + + +class Body(Relic): + stat: Stat + allowed_stats: list[Stat] = [ + Stat.HP_PERC, + Stat.ATK_PERC, + Stat.DEF_PERC, + Stat.EFFECTHITRATE, + Stat.OUTGOINGHEALING, + Stat.CRITRATE, + Stat.CRITDMG, + ] + + +class Feet(Relic): + stat: Stat + allowed_stats: list[Stat] = [ + Stat.HP_PERC, + Stat.ATK_PERC, + Stat.DEF_PERC, + Stat.SPD, + ] + + +class Sphere(Relic): + stat: Stat + allowed_stats: list[Stat] = [ + Stat.HP_PERC, + Stat.ATK_PERC, + Stat.DEF_PERC, + Stat.PHYSICALDMG, + Stat.FIREDMG, + Stat.ICEDMG, + Stat.WINDDMG, + Stat.LIGHTNINGDMG, + Stat.QUANTUMDMG, + Stat.IMAGINARYDMG, + ] + + +class Rope(Relic): + stat: Stat + allowed_stats: list[Stat] = [ + Stat.HP_PERC, + Stat.ATK_PERC, + Stat.DEF_PERC, + Stat.BREAKEFFECT, + Stat.ENERGYREGENERATIONRATE, + ] + + +class RelicToKeep(BaseModel): + position: Position + primary_stat: Stat + secondary_stat: Stat + tertiary_stat: Stat | None + characters: set[str] = set() + + + def __hash__(self) -> int: + return hash((self.primary_stat, self.secondary_stat, self.tertiary_stat)) + + def __eq__(self, other) -> bool: + return (self.primary_stat, self.secondary_stat, self.tertiary_stat) == ( + other.primary_stat, + other.secondary_stat, + other.tertiary_stat, + ) + + def add_character(self, character: str) -> None: + self.characters.add(character) diff --git a/src/star_rail_relic_trimmer/optimise.py b/src/star_rail_relic_trimmer/optimise.py new file mode 100644 index 0000000..6845784 --- /dev/null +++ b/src/star_rail_relic_trimmer/optimise.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from typing import Any, Generator + +from star_rail_relic_trimmer.models import Position, Relic, RelicToKeep, Stat + + +def main_stat_plus_stat( + relic: Relic, + stats: list[Stat], + character_name: str, + position: Position, +) -> Generator[RelicToKeep, Any, None]: + if position in (Position.Head, Position.Hands): + relics_to_keep = [ + RelicToKeep( + primary_stat=relic.stat, + secondary_stat=stats[0], + tertiary_stat=stats[i], + characters={character_name}, + position=position.value, + ) + for i in range(1, len(stats)) + ] + else: + relics_to_keep = [ + RelicToKeep( + primary_stat=relic.stat, + secondary_stat=stat, + tertiary_stat=None, + characters={character_name}, + position=position.value, + ) + for stat in stats + if relic.stat != stat + ] + yield from relics_to_keep + + +def no_main_stat_two_stats( + stats: list[Stat], + character_name: str, + position: Position, +) -> Generator[RelicToKeep, Any, None]: + relics_to_keep = [ + RelicToKeep( + primary_stat=Stat.ANY, + secondary_stat=stats[0], + tertiary_stat=stats[i], + characters={character_name}, + position=position.value, + ) + for i in range(1, 3) + ] + yield from relics_to_keep + + +def prune_any(relics: list[RelicToKeep]): + _relics = relics[:] + for _, item in enumerate(relics): + if item.position in (Position.Head, Position.Hands) and item.primary_stat == Stat.ANY: + _relics.pop(_relics.index(item)) + return _relics