195 lines
6.5 KiB
Python
195 lines
6.5 KiB
Python
# Copyright 2024 Zoé Cassiopée Gauthier.
|
|
#
|
|
# Use of this source code is governed by an MIT-style
|
|
# license that can be found in the LICENSE file or at
|
|
# https://opensource.org/licenses/MIT.
|
|
|
|
import urllib.parse
|
|
from dataclasses import dataclass
|
|
from functools import cached_property
|
|
from typing import NotRequired, TypedDict, cast, final
|
|
|
|
import requests_cache
|
|
|
|
from pogo_scaled_estimators.typing import PokemonType
|
|
|
|
BASE_URL = "https://fight.pokebattler.com"
|
|
WEAKNESS = 1.6
|
|
DOUBLE_WEAKNESS = WEAKNESS * WEAKNESS
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Raid:
|
|
tier: str
|
|
defender: str
|
|
level: int = 40
|
|
party: int = 1
|
|
|
|
|
|
@dataclass
|
|
class Move:
|
|
move_id: str
|
|
typing: PokemonType
|
|
|
|
|
|
@dataclass
|
|
class MovesetResult:
|
|
fast_move: str
|
|
charged_move: str
|
|
estimator: float
|
|
|
|
def scale(self, factor: float) -> "MovesetResult":
|
|
return MovesetResult(self.fast_move, self.charged_move, self.estimator / factor)
|
|
|
|
|
|
class PokebattlerMove(TypedDict):
|
|
moveId: str
|
|
type: PokemonType
|
|
|
|
|
|
class PokebattlerPokemon(TypedDict):
|
|
pokemonId: str
|
|
type: PokemonType
|
|
type2: NotRequired[PokemonType]
|
|
quickMoves: list[str]
|
|
cinematicMoves: list[str]
|
|
movesets: list[dict[str, str]]
|
|
|
|
|
|
class PokebattlerRaid(TypedDict):
|
|
pokemonId: str
|
|
|
|
|
|
class PokebattlerRaidTierInfo(TypedDict):
|
|
guessTier: str
|
|
|
|
|
|
class PokebattlerRaidTier(TypedDict):
|
|
tier: str
|
|
info: PokebattlerRaidTierInfo
|
|
raids: list[PokebattlerRaid]
|
|
|
|
|
|
@final
|
|
class PokebattlerProxy:
|
|
def __init__(self, log_level="INFO"):
|
|
self._cached_session = requests_cache.CachedSession("pokebatter_cache", cache_control=True, use_cache_dir=True)
|
|
|
|
@cached_property
|
|
def moves(self) -> list[PokebattlerMove]:
|
|
return self._cached_session.get(f"{BASE_URL}/moves").json()["move"]
|
|
|
|
@cached_property
|
|
def pokemon(self) -> list[PokebattlerPokemon]:
|
|
return self._cached_session.get(f"{BASE_URL}/pokemon").json()["pokemon"]
|
|
|
|
@cached_property
|
|
def raids(self) -> list[PokebattlerRaidTier]:
|
|
return self._cached_session.get(f"{BASE_URL}/raids").json()["tiers"]
|
|
|
|
@cached_property
|
|
def resists(self) -> dict[str, list[float]]:
|
|
return self._cached_session.get(f"{BASE_URL}/resists").json()
|
|
|
|
def simulate(self, raid: Raid) -> dict[str, list[MovesetResult]]:
|
|
query_string = {
|
|
"sort": "ESTIMATOR",
|
|
"weatherCondition": "NO_WEATHER",
|
|
"dodgeStrategy": "DODGE_REACTION_TIME",
|
|
"aggregation": "AVERAGE",
|
|
"includeLegendary": "true",
|
|
"includeShadow": "true",
|
|
"includeMegas": "true",
|
|
"primalAssistants": "",
|
|
"numParty": str(raid.party),
|
|
}
|
|
if raid.tier != "RAID_LEVEL_3":
|
|
query_string["friendshipLevel"] = "FRIENDSHIP_LEVEL_4"
|
|
url = f"{BASE_URL}/raids/defenders/{raid.defender}/levels/{raid.tier}/attackers/levels/{raid.level}/strategies/CINEMATIC_ATTACK_WHEN_POSSIBLE/DEFENSE_RANDOM_MC?{urllib.parse.urlencode(query_string, doseq=True)}"
|
|
response = self._cached_session.get(url)
|
|
results: dict[str, list[MovesetResult]] = {}
|
|
response_json = response.json()
|
|
for attacker in response_json["attackers"][0]["randomMove"]["defenders"]:
|
|
results[attacker["pokemonId"]] = [
|
|
MovesetResult(
|
|
attacker_moves["move1"], attacker_moves["move2"], cast(float, attacker_moves["result"]["estimator"])
|
|
)
|
|
for attacker_moves in attacker["byMove"]
|
|
]
|
|
return results
|
|
|
|
def raid_bosses(self, attacker_types: list[PokemonType]) -> dict[str, list[str]]:
|
|
raid_tiers: list[str] = []
|
|
raid_bosses: dict[str, list[str]] = {}
|
|
|
|
for raid_level in ["3", "5", "MEGA", "MEGA_5", "ULTRA_BEAST"]:
|
|
tier = f"RAID_LEVEL_{raid_level}"
|
|
raid_tiers.extend(
|
|
[
|
|
tier,
|
|
f"{tier}_LEGACY",
|
|
f"{tier}_FUTURE",
|
|
]
|
|
)
|
|
raid_bosses[tier] = []
|
|
|
|
for tier in filter(lambda tier: tier["tier"] in raid_tiers, self.raids):
|
|
for boss in (raid["pokemonId"] for raid in tier["raids"]):
|
|
if boss.endswith("_FORM"):
|
|
continue
|
|
boss_pokemon: PokebattlerPokemon = next(filter(lambda mon: mon["pokemonId"] == boss, self.pokemon))
|
|
boss_types: tuple[PokemonType, PokemonType] = (
|
|
boss_pokemon["type"],
|
|
boss_pokemon.get("type2", "POKEMON_TYPE_NONE"),
|
|
)
|
|
if any(self._is_weak(attacker_type, boss_types) for attacker_type in attacker_types):
|
|
raid_bosses[tier["info"]["guessTier"]].append(boss)
|
|
|
|
return raid_bosses
|
|
|
|
def _is_weak(self, attacker_type: PokemonType, defender_types: tuple[PokemonType, PokemonType]) -> bool:
|
|
pokemon_types = list(self.resists.keys())
|
|
defender_type_indices = (
|
|
pokemon_types.index(defender_types[0]),
|
|
pokemon_types.index(defender_types[1]),
|
|
)
|
|
attack_resist = (
|
|
self.resists[attacker_type][defender_type_indices[0]]
|
|
* self.resists[attacker_type][defender_type_indices[1]]
|
|
)
|
|
|
|
# Check for double weakness.
|
|
if attack_resist >= DOUBLE_WEAKNESS:
|
|
return True
|
|
|
|
# Checkout for single weakness if not double weak to anything else.
|
|
any_double_weaknesses = any(
|
|
r[defender_type_indices[0]] * r[defender_type_indices[1]] >= DOUBLE_WEAKNESS for r in self.resists.values()
|
|
)
|
|
if not any_double_weaknesses and attack_resist >= WEAKNESS:
|
|
return True
|
|
|
|
return False
|
|
|
|
def with_charged_moves(self, attacker_types: list[PokemonType]) -> list[str]:
|
|
charged_moves: list[str] = [
|
|
move["moveId"]
|
|
for move in self.moves
|
|
if "moveId" in move and "type" in move and move["type"] in attacker_types
|
|
]
|
|
return [
|
|
mon["pokemonId"]
|
|
for mon in self.pokemon
|
|
if any(moveset["cinematicMove"] in charged_moves for moveset in mon["movesets"])
|
|
]
|
|
|
|
def find_pokemon(self, name: str) -> PokebattlerPokemon:
|
|
return next(filter(lambda mon: mon["pokemonId"] == name, self.pokemon))
|
|
|
|
def pokemon_type(self, name: str) -> PokemonType:
|
|
return self.find_pokemon(name)["type"]
|
|
|
|
def find_move(self, move_id: str) -> Move:
|
|
move = next(filter(lambda move: "moveId" in move and move["moveId"] == move_id, self.moves))
|
|
return Move(move_id, move["type"])
|