Source code for rlapi.player

# Copyright 2018-present Jakub Kuczys (https://github.com/Jackenmen)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import annotations

import contextlib
from typing import TYPE_CHECKING, Any, Dict, Final, List, Optional, Union

from .enums import Platform, PlaylistKey, Stat
from .player_titles import PlayerTitle
from .tier_estimates import TierEstimates
from .typedefs import PlaylistBreakdownType, TierBreakdownType

if TYPE_CHECKING:
    from .client import Client

# The documentation of below constants needs to be manually repeated in docs/api.rst
# due to: https://github.com/sphinx-doc/sphinx/issues/6495

#: A sequence of rank names, indexed by the value of `Playlist.tier`.
#:
#: Rank names are only provided in the English language.
RANKS = (
    "Unranked",
    "Bronze I",
    "Bronze II",
    "Bronze III",
    "Silver I",
    "Silver II",
    "Silver III",
    "Gold I",
    "Gold II",
    "Gold III",
    "Platinum I",
    "Platinum II",
    "Platinum III",
    "Diamond I",
    "Diamond II",
    "Diamond III",
    "Champion I",
    "Champion II",
    "Champion III",
    "Grand Champion I",
    "Grand Champion II",
    "Grand Champion III",
    "Supersonic Legend",
)
#: A sequence of roman numerals for divisions,
#: indexed by the value of `Playlist.division`.
DIVISIONS = ("I", "II", "III", "IV")
#: A sequence of season reward level names, indexed by the value of
#: `SeasonRewards.level`.
#:
#: Season reward level names are only provided in the English language.
SEASON_REWARDS = (
    "Unranked",
    "Bronze",
    "Silver",
    "Gold",
    "Platinum",
    "Diamond",
    "Champion",
    "Grand Champion",
    "Supersonic Legend",
)
#: A sequence of playlists that can advance player's season rewards.
PLAYLISTS_WITH_SEASON_REWARDS = (
    PlaylistKey.solo_duel,
    PlaylistKey.doubles,
    PlaylistKey.standard,
    PlaylistKey.hoops,
    PlaylistKey.rumble,
    PlaylistKey.dropshot,
    PlaylistKey.snow_day,
)

__all__ = (
    "RANKS",
    "DIVISIONS",
    "SEASON_REWARDS",
    "PLAYLISTS_WITH_SEASON_REWARDS",
    "Playlist",
    "SeasonRewards",
    "Player",
)


[docs]class Playlist: """Playlist() Represents Rocket League playlist stats data. .. container:: operations ``str(x)`` Returns playlist's rank string, e.g. "Champion I Div III". This is only provided in the English language. Attributes ---------- key: `PlaylistKey` or `int` Playlist's key. Might be `int`, if that key is not within the ones recognised by the enumerator. tier: int Tier on this playlist. division: int Division on this playlist. mu: float Mu on this playlist. skill: int Skill rating on this playlist. sigma: float Sigma on this playlist. win_streak: int Win streak on this playlist. matches_played: int Number of matches played on this playlist during the current season. lifetime_matches_played: int Number of matches played on this playlist since player started playing the game. .. note:: This number does not seem to include some of the earlier seasons. It is estimated that it include all matches played in Season 4 (before Free To Play update) and later. placement_matches_played: int Number of placement matches played on this playlist during the current season. Maxes out at 10. breakdown: dict Playlist tier breakdown. tier_estimates: `TierEstimates` Tier estimates for this playlist. """ #: Max tier, i.e. Supersonic Legend TIER_MAX: Final[int] = 22 #: Max division, i.e. Division IV DIVISION_MAX: Final[int] = 3 __slots__ = ( "key", "tier", "division", "mu", "skill", "sigma", "win_streak", "matches_played", "lifetime_matches_played", "placement_matches_played", "breakdown", "tier_estimates", ) def __init__( self, *, breakdown: Optional[PlaylistBreakdownType] = None, playlist_key: Union[PlaylistKey, int], data: Dict[str, Any], ): self.key = playlist_key # only mu and sigma always exist, rest might be None or not be part of the dict self.tier: int = data.get("tier") or 0 self.division: int = data.get("division") or 0 mu: Optional[float] = data.get("mu") self.mu: float if mu is not None: self.mu = mu else: self.mu = 25 skill: Optional[int] = data.get("skill") self.skill: int if skill is not None: self.skill = skill else: self.skill = int(self.mu * 20 + 100) self.sigma: float = data.get("sigma") or 8.333 self.win_streak: int = data.get("win_streak") or 0 self.matches_played: int = data.get("matches_played") or 0 self.lifetime_matches_played: int = data.get("lifetime_matches_played") or 0 self.placement_matches_played: int = data.get("placement_matches_played") or 0 self.breakdown = breakdown if breakdown is not None else {} self.tier_estimates = TierEstimates(self) def __str__(self) -> str: try: if self.tier in {0, self.TIER_MAX}: return RANKS[self.tier] return f"{RANKS[self.tier]} Div {DIVISIONS[self.division]}" except IndexError: return "Unknown" def __repr__(self) -> str: return ( f"<{self.__class__.__name__}" f" {self.key};" f" Rank {self};" f" mu={self.mu}" f" skill={self.skill}" f" win_streak={self.win_streak}" f" matches_played={self.matches_played}" f">" )
[docs]class SeasonRewards: """SeasonRewards() Represents season rewards informations. Attributes ---------- level: int Player's season reward level. wins: int Player's season reward wins. can_advance: bool Tells if player can advance to `next_level`. next_level: int, optional Next level of season rewards or ``None`` if max level has already been reached. """ #: Max season reward level, i.e. Supersonic Legend MAX_LEVEL: Final[int] = 8 __slots__ = ("level", "wins", "can_advance", "next_level") def __init__(self, *, highest_tier: int = 0, data: Dict[str, Any]) -> None: self.level: int = data.get("level") or 0 self.wins: int = data.get("wins") or 0 self.can_advance: bool if self.level == 0 or self.level * 3 < highest_tier: self.can_advance = True else: self.can_advance = False self.next_level = self.level + 1 if self.level != self.MAX_LEVEL else None def __repr__(self) -> str: return ( f"<{self.__class__.__name__}" f" level={self.level}" f" wins={self.wins}" f" can_advance={self.can_advance}" f" next_level={self.next_level}" f">" )
[docs]class PlayerStats: """PlayerStats() Represents player stats (assists, goals, MVPs, etc.). .. container:: operations ``x[key]`` Lookup player's stat value by `Stat` enum. Attributes ---------- assists: int Number of player's assists. goals: int Number of player's goals. mvps: int Number of player's MVPs. saves: int Number of player's saves. shots: int Number of player's shots. wins: int Number of player's wins. """ __slots__ = ( "assists", "goals", "mvps", "saves", "shots", "wins", ) def __init__(self, data: List[Dict[str, Any]]) -> None: stats = {stat["stat_type"]: stat["value"] for stat in data} self.assists: int = stats.get("assists", 0) self.goals: int = stats.get("goals", 0) self.mvps: int = stats.get("mvps", 0) self.saves: int = stats.get("saves", 0) self.shots: int = stats.get("shots", 0) self.wins: int = stats.get("wins", 0) def __getitem__(self, stat: Stat) -> int: return int(getattr(self, stat.name)) def __repr__(self) -> str: attrs = " ".join(f"{key}={getattr(self, key)}" for key in self.__slots__) return f"<{self.__class__.__name__} {attrs}>"
[docs]class Player: """Player() Represents Rocket League Player Attributes ---------- platform: `Platform` Player's platform. user_id: str, optional Player's user ID. Present when the data was looked up by user ID or when the lookup platform was Steam or Epic Games. user_name: str Player's username (display name). playlists: dict Dictionary mapping `PlaylistKey` with `Playlist`. tier_breakdown: dict Tier breakdown. highest_tier: int Highest tier of the player. Doesn't include the playlists that don't count towards season rewards. season_rewards: `SeasonRewards` Season rewards info. stats: `PlayerStats` Player's stats (assists, goals, MVPs, etc.). """ __slots__ = ( "_client", "platform", "user_id", "user_name", "playlists", "tier_breakdown", "highest_tier", "season_rewards", "stats", ) def __init__( self, *, client: Client, tier_breakdown: Optional[TierBreakdownType] = None, platform: Platform, data: Dict[str, Any], ) -> None: self._client = client self.platform = platform self.user_id: Optional[str] = data.get("player_id") self.user_name: str = data["player_name"] self.playlists: Dict[Union[PlaylistKey, int], Playlist] = {} player_skills = data.get("player_skills", []) self.tier_breakdown = tier_breakdown if tier_breakdown is not None else {} self._prepare_playlists(player_skills) self.highest_tier = max( ( playlist.tier for playlist in self.playlists.values() if playlist.key in PLAYLISTS_WITH_SEASON_REWARDS ), default=0, ) season_rewards = data.get("season_rewards", {}) self.season_rewards = SeasonRewards( highest_tier=self.highest_tier, data=season_rewards ) player_stats = data.get("player_stats", []) self.stats = PlayerStats(player_stats) def __repr__(self) -> str: platform_repr = f"{self.platform.__class__.__name__}.{self.platform._name_}" return ( f"<{self.__class__.__name__}" f" platform={platform_repr}" f" user_id={self.user_id!r}" f" user_name={self.user_name!r}" f">" ) def __eq__(self, other: Any) -> bool: if not isinstance(other, self.__class__): return False if self.platform is not other.platform: return False # both object have `user_id` so we can just compare those if None not in (self.user_id, other.user_id): return self.user_id == other.user_id # it's rather unlikely that only one `user_id` is None if platforms are equal, # but checking equality of both `user_id` and `user_name` just in case return (self.user_id, self.user_name) == (other.user_id, other.user_name) def __hash__(self) -> int: if self.user_id is not None: return hash((self.platform, "by_user_id", self.user_id)) return hash((self.platform, "by_user_name", self.user_name))
[docs] async def titles(self) -> List[PlayerTitle]: """ Get player's titles. .. note:: Some titles that the player has may not be included in the response. Returns ------- `list` of `PlayerTitle` List of player's titles. Raises ------ HTTPException HTTP request to Rocket League failed. """ if self.platform in (Platform.steam, Platform.epic): assert self.user_id is not None return await self._client.get_player_titles(self.platform, self.user_id) return await self._client.get_player_titles(self.platform, self.user_name)
[docs] def get_playlist(self, playlist_key: PlaylistKey) -> Optional[Playlist]: """ Get playlist for the player. Parameters ---------- playlist_key: PlaylistKey `PlaylistKey` for playlist to get. Returns ------- `Playlist`, optional Playlist object for provided playlist key. """ return self.playlists.get(playlist_key)
def add_playlist(self, playlist: Dict[str, Any]) -> None: playlist_key = playlist.pop("playlist") breakdown = self.tier_breakdown.get(playlist_key, {}) with contextlib.suppress(ValueError): playlist_key = PlaylistKey(playlist_key) self.playlists[playlist_key] = Playlist( breakdown=breakdown, playlist_key=playlist_key, data=playlist ) def _prepare_playlists(self, player_skills: List[Dict[str, Any]]) -> None: for playlist in player_skills: self.add_playlist(playlist)