Пример #1
0
    async def champions_stats(self,
                              ctx: commands.Context,
                              user_id=None,
                              date_start=None):
        """Returns your games total and winrate for all champions.
        """
        try:
            player = await self.get_player_with_team_check(ctx, user_id)
        except PermissionError:
            return

        date_start = dateparser.parse(date_start) if date_start else date_start

        stats = player.get_champions_stats(date_start)

        table = []
        for champion_id in stats:
            table.append([
                lit.get_name(champion_id) if champion_id else "Unknown",
                f"{stats[champion_id].role.capitalize()}",
                stats[champion_id].games,
                f"{stats[champion_id].wins / stats[champion_id].games * 100:.1f}%",
            ])

        # Sorting the table by games total
        table = sorted(table, key=lambda x: -x[2])
        # Adding the header last to not screw with the sorting
        table.insert(0, ["Champion", "Role", "Games", "Winrate"])

        await ctx.send(f'```{tabulate(table, headers="firstrow")}```')
Пример #2
0
    async def history(self,
                      ctx: commands.Context,
                      user_id=None,
                      display_games=20):
        """
        Returns your match history in a table.

        If user_id is supplied, shows the user’s match history. Requires being on the same team or admin.
        display_games specifies how many games to show and is 20 by default.
        """
        try:
            player = await self.get_player_with_team_check(ctx, user_id)
        except PermissionError:
            return

        games_list = player.get_latest_games(display_games)

        table = [["Game ID", "Date", "Role", "Champion", "Result"]]
        for game, participant in games_list:
            table.append([
                game.id,
                game.date.date(),
                participant.role,
                lit.get_name(participant.champion_id)
                if participant.champion_id else "Unknown",
                "Win" if game.winner == participant.team else "Loss",
            ])

        await ctx.send(f'```{tabulate(table, headers="firstrow")}```')
Пример #3
0
def test_parse_summoners():
    data = parsing.parse_summoners(latest_version)

    # Not that many summoners
    assert len(data) > 5

    for id_, name in data.items():
        # We test using the *old* parser!
        assert name == lol_id_tools.get_name(id_, object_type="summoner_spell")
Пример #4
0
def test_parse_items():
    data = parsing.parse_items(latest_version)

    # More items than Pokemons
    assert len(data) > 200

    for id_, name in data.items():
        # We test using the *old* parser!
        assert name == lol_id_tools.get_name(id_, object_type="item")
Пример #5
0
def test_parse_runes():
    data = parsing.parse_runes(latest_version)

    # Not that many runes really
    assert len(data) > 5 * 9

    for id_, name in data.items():
        # We test using the *old* parser!
        assert name == lol_id_tools.get_name(id_, object_type="rune")
Пример #6
0
def test_get_name_with_patch(tuple):
    id_, name, type_, patch = tuple

    response = lol_id_tools.get_name(
        id_,
        object_type=type_,
        patch=patch,
    )

    assert response == name
Пример #7
0
def test_botrk_id():
    # Base case
    assert lit.get_id("Blade of the Ruined King") == 3153
    # Case sensitivity test
    assert lit.get_id("Blade of the ruined king") == 3153
    # Typo test
    assert lit.get_id("Blade of the kuined ring") == 3153
    # Korean test
    assert lit.get_id("몰락한 왕의 검") == 3153
    # Nickname test
    assert lit.get_id("botrk") == 3153

    # Name test
    assert lit.get_name(3153, object_type="item") == "Blade of the Ruined King"
Пример #8
0
def test_grasp_id():
    # Base case
    assert lit.get_id("Grasp of the Undying") == 8437
    # Case sensitivity test
    assert lit.get_id("grasp of the undying") == 8437
    # Shorthand test
    assert lit.get_id("undying") == 8437
    # Typo test
    assert lit.get_id("graps of the undying") == 8437
    # Korean test
    assert lit.get_id("착취의 손아귀") == 8437

    # Name test
    assert lit.get_name(8437, object_type="rune") == "Grasp of the Undying"
Пример #9
0
def get_champion_emoji(champion_id: Optional[int], bot) -> str:
    if champion_id is None:
        return "❔"

    champion_name = lol_id_tools.get_name(champion_id, object_type="champion")

    emoji_name = no_symbols_regex.sub("", champion_name).replace(" ", "")

    for emoji in bot.emojis:
        emoji: Emoji
        if emoji.name == emoji_name:
            return str(emoji)

    return champion_name
Пример #10
0
def test_botrk_id():
    # Base case
    assert lit.get_id("Blade of the Ruined King") == 3153
    # Case sensitivity test
    assert lit.get_id("Blade of the ruined king") == 3153
    # Typo test
    assert lit.get_id("Blade of the kuined ring") == 3153
    # OBSOLETE - We don't support nicknames or different locales anymore
    # Korean test
    # assert lit.get_id("몰락한 왕의 검") == 3153
    # Nickname test
    # assert lit.get_id("botrk") == 3153

    # Name test
    assert lit.get_name(3153, object_type="item") == "Blade of The Ruined King"
Пример #11
0
def test_mf_id():
    # Base case
    assert lit.get_id("Miss Fortune", object_type="champion") == 21
    # Case sensitivity test
    assert lit.get_id("missfortune", object_type="champion") == 21
    # Typo test
    assert lit.get_id("misforune", object_type="champion") == 21

    # Different languages test
    assert lit.get_id("미스 포츈", input_locale="ko_KR") == 21
    assert lit.get_id("미스 포츈", input_locale="korean") == 21
    assert lit.get_id("missfortune", input_locale="french") == 21

    # Nickname test
    assert lit.get_id("MF") == 21

    # Get name from ID test
    assert lit.get_name(21, object_type="champion") == "Miss Fortune"
Пример #12
0
def get_champion_emoji(emoji_input: Optional[Union[int, str]], bot) -> str:
    """
    Accepts champion IDs, "loading", and None
    """
    emoji_name = None
    fallback = None

    if emoji_input is None:
        return "❔"
    elif emoji_input == "loading":
        emoji_name = emoji_input
        fallback = "❔"
    elif type(emoji_input) == int:
        fallback = lol_id_tools.get_name(emoji_input, object_type="champion")
        emoji_name = no_symbols_regex.sub("", fallback).replace(" ", "")

    for emoji in bot.emojis:
        emoji: Emoji
        if emoji.name == emoji_name:
            return str(emoji)

    # Fallback that should only be reached when we don’t find the rights emoji
    return fallback
Пример #13
0
def add_runes(player: lol_dto.classes.game.LolGamePlayer, runes_info, patch, add_names=True):
    player_runes = next(p for p in runes_info if p["hero_id_"] == player["championId"])

    player["runes"] = []
    for rune_index, rune in enumerate(player_runes["runes_info_"]["runes_list_"]):
        # slot is 0 for keystones, 1 for first rune of primary tree, ...
        # rank is 1 for most keystones, except stat perks that can be 2
        rune = lol_dto.classes.game.LolGamePlayerRune(id=rune["runes_id_"], slot=rune_index, rank=rune["runes_num_"],)

        if add_names:
            rune["name"] = lit.get_name(rune["id"], object_type="rune")

        player["runes"].append(rune)

    # We need patch information to properly load rune tree names
    if patch:
        player["primaryRuneTreeId"], player["primaryRuneTreeName"] = rune_tree_handler.get_primary_tree(
            player["runes"], patch
        )
        (player["secondaryRuneTreeId"], player["secondaryRuneTreeName"],) = rune_tree_handler.get_secondary_tree(
            player["runes"], patch
        )

    return player
Пример #14
0
def match_to_game(match_dto: dict, add_names: bool = False) -> game_dto.LolGame:
    """Returns a LolGame from a MatchDto.

    Args:
        match_dto: A MatchDto from Riot’s API
        add_names: whether or not to add names for human readability in the DTO. False by default.

    Returns:
        The LolGame representation of the game.
    """
    riot_source = {
        "riotLolApi": RiotGameIdentifier(
            gameId=match_dto["gameId"], platformId=match_dto["platformId"]
        )
    }

    log_prefix = (
        f"gameId {match_dto['gameId']}|" f"platformId {match_dto['platformId']}:\t"
    )
    info_log = set()

    date_time = datetime.utcfromtimestamp(match_dto["gameCreation"] / 1000)
    date_time = date_time.replace(tzinfo=timezone.utc)
    iso_date = date_time.isoformat(timespec="seconds")

    patch = ".".join(match_dto["gameVersion"].split(".")[:2])
    winner = (
        "BLUE"
        if (match_dto["teams"][0]["teamId"] == 100)
        == (match_dto["teams"][0]["win"] == "Win")
        else "RED"
    )

    # TODO Change optional fields to .get() instead of [], do it in timeline too

    game = game_dto.LolGame(
        sources=riot_source,
        duration=match_dto["gameDuration"],
        start=iso_date,
        patch=patch,
        gameVersion=match_dto["gameVersion"],
        winner=winner,
        teams={},
    )

    for team in match_dto["teams"]:
        side = "BLUE" if team["teamId"] == 100 else "RED"

        # TODO Handle old games with elemental drakes before they were part of the API
        team_dto = game_dto.LolGameTeam(
            endOfGameStats=game_dto.LolGameTeamEndOfGameStats(
                riftHeraldKills=team.get("riftHeraldKills"),
                dragonKills=team.get("dragonKills"),
                baronKills=team.get("baronKills"),
                towerKills=team.get("towerKills"),
                inhibitorKills=team.get("inhibitorKills"),
                firstTower=team.get("firstTower"),
                firstInhibitor=team.get("firstInhibitor"),
                firstRiftHerald=team.get("firstRiftHerald"),
                firstDragon=team.get("firstDragon"),
                firstBaron=team.get("firstBaron"),
            )
        )

        team_dto["bans"] = [b["championId"] for b in team["bans"]]

        team_dto["players"] = []

        for participant in match_dto["participants"]:
            if participant["teamId"] != team["teamId"]:
                continue

            try:
                participant_identity = next(
                    identity["player"]
                    for identity in match_dto["participantIdentities"]
                    if identity["participantId"] == participant["participantId"]
                )
                # Esports matches do not have an accountId field
                if "accountId" in participant_identity:
                    unique_identifier = {
                        "riotLolApi": {
                            "accountId": participant_identity["accountId"],
                            "platformId": participant_identity["platformId"],
                        }
                    }
                else:
                    unique_identifier = {}

            # Custom games don’t have identity info
            except KeyError:
                participant_identity = None
                unique_identifier = {}

            # TODO Make that backwards-compatible with pre-runes reforged games
            runes = [
                game_dto.LolGamePlayerRune(
                    id=participant["stats"].get(f"perk{i}"),
                    slot=i,
                    stats=[
                        participant["stats"].get(f"perk{i}Var{j}") for j in range(1, 4)
                    ],
                )
                for i in range(0, 6)
            ]

            # Adding stats perks
            runes.extend(
                [
                    game_dto.LolGamePlayerRune(
                        id=participant["stats"].get(f"statPerk{i}"), slot=i + 6,
                    )
                    for i in range(0, 3)
                ]
            )

            items = [
                game_dto.LolGamePlayerItem(
                    id=participant["stats"].get(f"item{i}"), slot=i
                )
                for i in range(0, 7)
            ]

            summoner_spells = [
                game_dto.LolGamePlayerSummonerSpell(
                    id=participant.get(f"spell{i}Id"), slot=i - 1
                )
                for i in range(1, 3)
            ]

            end_of_game_stats = game_dto.LolGamePlayerEndOfGameStats(
                items=items,
                firstBlood=participant["stats"].get("firstBloodKill"),
                firstBloodAssist=participant["stats"].get(
                    "firstBloodAssist"
                ),  # This field is wrong by default
                kills=participant["stats"].get("kills"),
                deaths=participant["stats"].get("deaths"),
                assists=participant["stats"].get("assists"),
                gold=participant["stats"].get("goldEarned"),
                # TODO Test with older games
                cs=int(participant["stats"].get("totalMinionsKilled") or 0)
                + int(participant["stats"].get("neutralMinionsKilled") or 0),
                level=participant["stats"].get("champLevel"),
                wardsPlaced=participant["stats"].get("wardsPlaced"),
                wardsKilled=participant["stats"].get("wardsKilled"),
                visionWardsBought=participant["stats"].get("visionWardsBoughtInGame"),
                visionScore=participant["stats"].get("visionScore"),
                killingSprees=participant["stats"].get("killingSprees"),
                largestKillingSpree=participant["stats"].get("largestKillingSpree"),
                doubleKills=participant["stats"].get("doubleKills"),
                tripleKills=participant["stats"].get("tripleKills"),
                quadraKills=participant["stats"].get("quadraKills"),
                pentaKills=participant["stats"].get("pentaKills"),
                monsterKills=participant["stats"].get("neutralMinionsKilled"),
                monsterKillsInAlliedJungle=participant["stats"].get(
                    "neutralMinionsKilledTeamJungle"
                ),
                monsterKillsInEnemyJungle=participant["stats"].get(
                    "neutralMinionsKilledEnemyJungle"
                ),
                totalDamageDealt=participant["stats"].get("totalDamageDealt"),
                physicalDamageDealt=participant["stats"].get("physicalDamageDealt"),
                magicDamageDealt=participant["stats"].get("magicDamageDealt"),
                totalDamageDealtToChampions=participant["stats"].get(
                    "totalDamageDealtToChampions"
                ),
                physicalDamageDealtToChampions=participant["stats"].get(
                    "physicalDamageDealtToChampions"
                ),
                magicDamageDealtToChampions=participant["stats"].get(
                    "magicDamageDealtToChampions"
                ),
                damageDealtToObjectives=participant["stats"].get(
                    "damageDealtToObjectives"
                ),
                damageDealtToTurrets=participant["stats"].get("damageDealtToTurrets"),
                totalDamageTaken=participant["stats"].get("totalDamageTaken"),
                physicalDamageTaken=participant["stats"].get("physicalDamageTaken"),
                magicDamageTaken=participant["stats"].get("magicalDamageTaken"),
                longestTimeSpentLiving=participant["stats"].get(
                    "longestTimeSpentLiving"
                ),
                largestCriticalStrike=participant["stats"].get("largestCriticalStrike"),
                goldSpent=participant["stats"].get("goldSpent"),
                totalHeal=participant["stats"].get("totalHeal"),
                totalUnitsHealed=participant["stats"].get("totalUnitsHealed"),
                damageSelfMitigated=participant["stats"].get("damageSelfMitigated"),
                totalTimeCCDealt=participant["stats"].get("totalTimeCrowdControlDealt"),
                timeCCingOthers=participant["stats"].get("timeCCingOthers"),
            )

            # The following fields have proved to be missing or buggy in multiple games

            if "firstTowerKill" in participant["stats"]:
                end_of_game_stats["firstTower"] = participant["stats"]["firstTowerKill"]
                end_of_game_stats["firstTowerAssist"] = participant["stats"].get(
                    "firstTowerAssist"
                )
            else:
                info_log.add(f"{log_prefix}Missing ['player']['firstTower']")

            if "firstInhibitorKill" in participant["stats"]:
                end_of_game_stats["firstInhibitor"] = participant["stats"][
                    "firstInhibitorKill"
                ]
                end_of_game_stats["firstInhibitorAssist"] = participant["stats"].get(
                    "firstInhibitorAssist"
                )
            else:
                info_log.add(f"{log_prefix}Missing ['player']['firstInhibitor']")

            player = game_dto.LolGamePlayer(
                id=participant["participantId"],
                championId=participant["championId"],
                uniqueIdentifiers=unique_identifier,
                primaryRuneTreeId=participant["stats"].get("perkPrimaryStyle"),
                secondaryRuneTreeId=participant["stats"].get("perkSubStyle"),
                runes=runes,
                summonerSpells=summoner_spells,
                endOfGameStats=end_of_game_stats,
            )

            if participant_identity:
                player["inGameName"] = participant_identity["summonerName"]
                player["profileIconId"] = participant_identity["profileIcon"]

            # roleml compatibility
            if "role" in participant:
                # TODO Remove that after roleml refactor
                if participant["role"] not in {"TOP", "JGL", "MID", "BOT", "SUP"}:
                    participant["role"] = {
                        "top": "TOP",
                        "jungle": "JGL",
                        "mid": "MID",
                        "bot": "BOT",
                        "supp": "SUP",
                    }[participant["role"]]
                player["role"] = participant["role"]

            # Then, we add convenience name fields for human readability if asked
            if add_names:
                player["championName"] = lit.get_name(
                    player["championId"], object_type="champion"
                )
                player["primaryRuneTreeName"] = lit.get_name(
                    player["primaryRuneTreeId"], object_type="rune"
                )
                player["secondaryRuneTreeName"] = lit.get_name(
                    player["secondaryRuneTreeId"], object_type="rune"
                )

                for item in player["endOfGameStats"]["items"]:
                    item["name"] = lit.get_name(item["id"], object_type="item")
                for rune in player["runes"]:
                    rune["name"] = lit.get_name(rune["id"], object_type="rune")
                for summoner_spell in player["summonerSpells"]:
                    summoner_spell["name"] = lit.get_name(
                        summoner_spell["id"], object_type="summoner_spell"
                    )

            team_dto["players"].append(player)

        # We want to make extra sure players are always ordered by Riot’s given id
        team_dto["players"] = sorted(team_dto["players"], key=lambda x: x["id"])

        if add_names:
            team_dto["bansNames"] = []
            for ban in team_dto["bans"]:
                team_dto["bansNames"].append(lit.get_name(ban, object_type="champion"))

        game["teams"][side] = team_dto

    return game
Пример #15
0
def test_summoner_spell():
    assert lit.get_name(21, object_type="summoner_spell") == "Barrier"
Пример #16
0
def test_perks():
    assert lit.get_name(5002) == "Armor"
Пример #17
0
def test_keystone():
    assert lit.get_name(8000) == "Precision"
Пример #18
0
def test_jungle_item():
    assert lit.get_name(1400) == "Stalker's Blade - Warrior"
Пример #19
0
def parse_player_battle_data(
    player: lol_dto.classes.game.LolGamePlayer, player_battle_data: dict, add_names: bool
) -> List[str]:

    missing_fields = []

    try:
        # We start by verifying champion ID is coherent
        assert player["championId"] == int(player_battle_data["hero"])
    except AssertionError:
        # In the games with buggy championId, almost all fields were empty, we raise
        raise ValueError

    end_of_game_stats = lol_dto.classes.game.LolGamePlayerEndOfGameStats(
        kills=int(player_battle_data["kill"]),
        deaths=int(player_battle_data["death"]),
        assists=int(player_battle_data["assist"]),
        gold=int(player_battle_data["gold"]),
        cs=int(player_battle_data["lasthit"]),
        level=int(player_battle_data["level"]),
        items=[],
        # We cast boolean statistics as proper booleans
        firstBlood=bool(player_battle_data["firstBlood"]),
        firstTower=bool(player_battle_data["firstTower"]),
        # Then we add other numerical statistics
        killingSprees=int(player_battle_data["killingSprees"]),
        doubleKills=int(player_battle_data["dKills"]),
        tripleKills=int(player_battle_data["tKills"]),
        quadraKills=int(player_battle_data["qKills"]),
        pentaKills=int(player_battle_data["pKills"]),
        towerKills=int(player_battle_data["towerKills"]),
        monsterKills=int(player_battle_data["neutralKilled"]),
        monsterKillsInAlliedJungle=int(player_battle_data["neutralKilledTJungle"]),
        monsterKillsInEnemyJungle=int(player_battle_data["neutralKilledEJungle"]),
        wardsPlaced=int(player_battle_data["wardsPlaced"]),
        wardsKilled=int(player_battle_data["wardsKilled"]),
        visionWardsBought=int(player_battle_data["visionWardsBought"]),
        # Damage totals, using a nomenclature close to match-v4
        totalDamageDealt=int(player_battle_data["totalDamage"]),
        totalDamageDealtToChampions=int(player_battle_data["totalDamageToChamp"]),
        physicalDamageDealtToChampions=int(player_battle_data["pDamageToChamp"]),
        magicDamageDealtToChampions=int(player_battle_data["mDamageDealtToChamp"]),
        totalDamageTaken=int(player_battle_data["totalDamageTaken"]),
        physicalDamageTaken=int(player_battle_data["pDamageTaken"]),
        magicDamageTaken=int(player_battle_data["mDamageTaken"]),
    )

    for item_key in player_battle_data["equip"]:
        item = lol_dto.classes.game.LolGamePlayerItem(
            id=int(player_battle_data["equip"][item_key]), slot=int(item_key[-1]),
        )

        if add_names:
            item["name"] = lit.get_name(item["id"], object_type="item")

        end_of_game_stats["items"].append(item)

    # We validate the fields from sMatchMember
    assert end_of_game_stats["kills"] == player["endOfGameStats"]["kills"]
    assert end_of_game_stats["deaths"] == player["endOfGameStats"]["deaths"]
    assert end_of_game_stats["assists"] == player["endOfGameStats"]["assists"]
    assert end_of_game_stats["gold"] == player["endOfGameStats"]["gold"]

    # We check fields that are regularly missing

    for field_name in ["largestCriticalStrike", "largestKillingSpree", "inhibitorKills", "totalHeal"]:
        if field_name in player_battle_data:
            end_of_game_stats[field_name] = int(player_battle_data[field_name])
        else:
            missing_fields.append(field_name)

    if "pDamageDealt" in player_battle_data:
        end_of_game_stats["physicalDamageDealt"] = int(player_battle_data["pDamageDealt"])
    else:
        missing_fields.append("pDamageDealt")

    if "mDamageDealt" in player_battle_data:
        end_of_game_stats["magicDamageDealt"] = int(player_battle_data["mDamageDealt"])
    else:
        missing_fields.append("mDamageDealt")

    # Finally, we write them to the player object
    player["endOfGameStats"] = end_of_game_stats

    player["summonerSpells"] = []

    for skill_key in "skill-1", "skill-2":
        summoner_spell = lol_dto.classes.game.LolGamePlayerSummonerSpell(
            id=int(player_battle_data[skill_key]), slot=int(skill_key[-1]),
        )

        if add_names:
            summoner_spell["name"] = lit.get_name(summoner_spell["id"], object_type="summoner_spell")

        player["summonerSpells"].append(summoner_spell)

    return missing_fields
Пример #20
0
def parse_qq_game(qq_game_id: int, patch: str = None, add_names: bool = True) -> lol_dto.classes.game.LolGame:
    """Parses a QQ game and returns a LolGameDto.

    Params:
        qq_game_̤d: the qq game id, acquired from qq’s match list endpoint.
        patch: optional patch to include in the object and query rune trees.
        add_names: whether or not to add champions/items/runes names next to their objects through lol_id_tools.

    Returns:
        A LolGameDto.
    """

    game_info, team_info, runes_info, qq_server_id, qq_battle_id = get_all_qq_game_info(qq_game_id)

    log_prefix = (
        f"match {game_info['sMatchInfo']['bMatchId']}|"
        f"game {game_info['sMatchInfo']['MatchNum']}|"
        f"id {qq_game_id}:\t"
    )
    logging.debug(f"{log_prefix}Starting parsing")

    # We start by building the root of the game object
    lol_game_dto = lol_dto.classes.game.LolGame(
        sources={"qq": SourceQQ(id=int(qq_game_id), serverId=qq_server_id, battleId=qq_battle_id)},
        gameInSeries=int(game_info["sMatchInfo"]["MatchNum"]),
        teams={},
    )

    warnings = set()
    info = set()

    try:
        # The 'BattleTime' field sometimes has sub-second digits that we cut
        date_time = dateparser.parse(
            f"{game_info['battleInfo']['BattleDate']}T{game_info['battleInfo']['BattleTime'][:8]}"
        )
        date_time = date_time.replace(tzinfo=datetime.timezone(datetime.timedelta(hours=8)))
        lol_game_dto["start"] = date_time.isoformat(timespec="seconds")
    except KeyError:
        info.add(f"{log_prefix}Missing ['game']['start']")

    if patch:
        lol_game_dto["patch"] = patch

    # Get blue and red team IDs through the BlueTeam field in the first JSON
    blue_team_id = int(game_info["sMatchInfo"]["BlueTeam"])
    red_team_id = int(
        game_info["sMatchInfo"]["TeamA"]
        if game_info["sMatchInfo"]["BlueTeam"] == game_info["sMatchInfo"]["TeamB"]
        else game_info["sMatchInfo"]["TeamB"]
    )

    # TODO Write that in a more beautiful way
    # The MatchWin field refers to TeamA/TeamB, which are not blue/red
    if game_info["sMatchInfo"]["MatchWin"] == "1":
        if int(game_info["sMatchInfo"]["TeamA"]) == blue_team_id:
            lol_game_dto["winner"] = "BLUE"
        else:
            lol_game_dto["winner"] = "RED"
    elif game_info["sMatchInfo"]["MatchWin"] == "2":
        if int(game_info["sMatchInfo"]["TeamB"]) == blue_team_id:
            lol_game_dto["winner"] = "BLUE"
        else:
            lol_game_dto["winner"] = "RED"

    # Handle battle_data parsing and game-related information
    try:
        # This is a json inside the json of game_info
        battle_data = json.loads(game_info["battleInfo"]["BattleData"])
        lol_game_dto["duration"] = int(battle_data["game-period"])

    except JSONDecodeError:
        # Usually means the field was empty
        battle_data = {}

    # Match name usually look like TES vs EDG but some casing typos can be caught here
    possible_team_names = game_info["sMatchInfo"]["bMatchName"].lower().split("vs")  # Used if team_info is not there
    possible_team_names = [n.replace(" ", "").upper() for n in possible_team_names]

    # We iterate of the two team IDs from sMatchInfo
    for team_id in blue_team_id, red_team_id:
        team_color = "BLUE" if team_id == blue_team_id else "RED"

        team = lol_dto.classes.game.LolGameTeam(uniqueIdentifiers={"qq": {"id": team_id}}, players=[])

        # We start by getting as much information as possible from the sMatchMember fields
        for match_member in game_info["sMatchMember"]:
            # We match players on the team id
            if int(match_member["TeamId"]) != team_id:
                continue

            player = lol_dto.classes.game.LolGamePlayer(
                inGameName=match_member["GameName"],
                role=roles[match_member["Place"]],
                championId=int(match_member["ChampionId"]),
                endOfGameStats=lol_dto.classes.game.LolGamePlayerEndOfGameStats(
                    kills=int(match_member["Game_K"]),
                    deaths=int(match_member["Game_D"]),
                    assists=int(match_member["Game_A"]),
                    gold=int(match_member["Game_M"]),
                ),
                uniqueIdentifiers={
                    "qq": {"accountId": int(match_member["AccountId"]), "memberId": int(match_member["MemberId"])}
                },
            )

            if add_names:
                player["championName"] = lit.get_name(player["championId"], object_type="champion")

            team["players"].append(player)

            # We get the tentative team name
            # We cast team names and player names as lowercase because they made the mistake in some old games
            tentative_team_name = next(t for t in possible_team_names if t.lower() in match_member["GameName"].lower())

        # If we have info from the second endpoint we use it to validate team id and game winner
        if team_info:
            try:
                assert team_id == team_info[f"{team_color.lower()}_clan_id_"]

                team["name"] = team_info[f"{team_color.lower()}_clan_name_"]

                if team_id == team_info["win_clan_id_"]:
                    assert lol_game_dto["winner"] == team_color

            except AssertionError:
                warnings.add(f"{log_prefix}⚠ Inconsistent team sides between endpoints ⚠")
                info.add(f"{log_prefix}Team sides might be wrong")

                # We use the tentative team name from the first object instead
                team["name"] = tentative_team_name

                try:
                    # In the games with this issue, the second endpoint was the one with the right side information
                    team_color = next(
                        color for color in ("blue", "red") if team_info[f"{color}_clan_id_"] == team_id
                    ).upper()
                except StopIteration:
                    # When we cannot find a team with the same ID as the one in the first object, we drop the process
                    continue

                if team_info["win_clan_id_"] == team_id:
                    lol_game_dto["winner"] = team_color

        # If it is missing, we log what we are missing and try to guess team names from players
        else:
            info.add(f"{log_prefix}Missing ['team']['name'], guessing them from game name")
            info.add(f"{log_prefix}Missing ['player']['runes']")

            team["name"] = tentative_team_name

        # Without "battle data", we stop there
        # We also check both teams have the "players" field as we found games without it, and battleData was faulty
        if not battle_data or any(len(battle_data[team_side]["players"]) != 5 for team_side in ("left", "right")):
            warnings.add(f"{log_prefix}⚠ Missing 'BattleData', meaning almost all end of game stats ⚠")
            lol_game_dto["teams"][team_color] = team
            continue

        # Finding left/right team side through player names
        # TODO Make that a bit more palatable
        for tentative_team_side in "left", "right":
            # We just look at the first player
            player = battle_data[tentative_team_side]["players"][0]

            for match_member in game_info["sMatchMember"]:
                if player["name"] == match_member["GameName"] and int(match_member["TeamId"]) == team_id:
                    team_side = tentative_team_side

        # Sometimes the firstTower field isn’t in battleData but it can be calculated from the players
        if "firstTower" in battle_data[team_side]:
            first_tower = bool(battle_data[team_side]["firstTower"])
        else:
            first_tower = True in (bool(player["firstTower"]) for player in battle_data[team_side]["players"])

        # We fill more team related information
        team["bans"] = [
            int(battle_data[team_side][f"ban-hero-{ban_number}"])
            for ban_number in range(1, 6)
            if f"ban-hero-{ban_number}" in battle_data[team_side]
        ]

        team["endOfGameStats"] = lol_dto.classes.game.LolGameTeamEndOfGameStats(
            towerKills=int(battle_data[team_side]["tower"]),
            baronKills=int(battle_data[team_side]["b-dragon"]),
            dragonKills=int(battle_data[team_side]["s-dragon"]),
            firstTower=first_tower,
        )

        # We add ban names from the bans field
        if add_names:
            team["bansNames"] = [lit.get_name(i, object_type="champion") for i in team["bans"]]

        # Bans are sometimes incomplete
        if team["bans"].__len__() < 5:
            info.add(f"{log_prefix}Missing ['team']['bans']")

        # Finally, we look at per-player BattleData
        for player_battle_data in battle_data[team_side]["players"]:
            player = next(p for p in team["players"] if player_battle_data["name"] == p["inGameName"])

            # Updating missing fields for logging
            try:
                info.update(
                    [
                        f"{log_prefix}Missing ['player'][{field}]"
                        for field in parse_player_battle_data(player, player_battle_data, add_names)
                    ]
                )
            except ValueError:
                warnings.add(f"{log_prefix}⚠ Missing most end of game stats ⚠")
                warnings.add(f"{log_prefix}⚠ Missing runes ⚠")
                warnings.add(f"{log_prefix}⚠ Bans are likely wrong ⚠")
                continue

            try:
                add_runes(player, runes_info, patch, add_names)
            except StopIteration:
                info.add(f"{log_prefix}Missing ['player']['runes']")

        # Finally, we insert the team
        lol_game_dto["teams"][team_color] = team

    for warning in warnings:
        logging.warning(warning)

    for log in info:
        logging.info(log)

    return lol_game_dto
Пример #21
0
def match_timeline_to_game(
    match_timeline_dto: dict,
    game_id: int,
    platform_id: str,
    add_names: bool = False,
) -> game_dto.LolGame:
    """Returns a LolGame from a MatchTimelineDto.

    Args:
        match_timeline_dto: A MatchTimelineDto from Riot’s API.
        game_id: The gameId of the game, required as it is not present in the MatchTimelineDto.
        platform_id: The platformId of the game, required as it is not present in the MatchTimelineDto.
        add_names: whether or not to add names for human readability in the DTO. False by default.

    Returns:
        The LolGame representation of the game.
    """

    riot_source = {
        "riotLolApi": RiotGameIdentifier(gameId=game_id,
                                         platformId=platform_id)
    }

    # Creating the game_dto skeleton
    game = game_dto.LolGame(
        sources=riot_source,
        teams={
            "BLUE":
            game_dto.LolGameTeam(
                players=[
                    game_dto.LolGamePlayer(id=i,
                                           snapshots=[],
                                           itemsEvents=[],
                                           wardsEvents=[],
                                           skillsLevelUpEvents=[])
                    for i in range(1, 6)
                ],
                monstersKills=[],
                buildingsKills=[],
            ),
            "RED":
            game_dto.LolGameTeam(
                players=[
                    game_dto.LolGamePlayer(id=i,
                                           snapshots=[],
                                           itemsEvents=[],
                                           wardsEvents=[],
                                           skillsLevelUpEvents=[])
                    for i in range(6, 11)
                ],
                monstersKills=[],
                buildingsKills=[],
            ),
        },
        kills=[],
    )

    for frame in match_timeline_dto["frames"]:
        # We start by adding player information at the given snapshot timestamps
        for participant_frame in frame["participantFrames"].values():
            team_side = "BLUE" if participant_frame[
                "participantId"] < 6 else "RED"

            # Finding the player with the same id in our game object
            player = next(p for p in game["teams"][team_side]["players"]
                          if p["id"] == participant_frame["participantId"])

            try:
                position = game_dto.Position(
                    x=participant_frame["position"]["x"],
                    y=participant_frame["position"]["y"])
            except KeyError:
                position = None

            snapshot = game_dto.LolGamePlayerSnapshot(
                timestamp=frame["timestamp"] / 1000,
                currentGold=participant_frame["currentGold"],
                totalGold=participant_frame["totalGold"],
                xp=participant_frame["xp"],
                level=participant_frame["level"],
                cs=participant_frame["minionsKilled"] +
                participant_frame["jungleMinionsKilled"],
                monstersKilled=participant_frame["jungleMinionsKilled"],
                position=position,
                # Next fields gotten with .get() so they are None if they haven’t been created by roleml
                totalGoldDiff=participant_frame.get("totalGoldDiff"),
                xpDiff=participant_frame.get("xpDiff"),
                csDiff=participant_frame.get("minionsKilledDiff"),
                monstersKilledDiff=participant_frame.get(
                    "jungleMinionsKilledDiff"),
            )

            player["snapshots"].append(snapshot)

        for event in frame["events"]:
            timestamp = event["timestamp"] / 1000

            # Epic monsters kills
            if event["type"] == "ELITE_MONSTER_KILL":
                if event["killerId"] < 1:
                    # This is Rift Herald killing itself, we just pass
                    riot_transmute_logger.debug(
                        f"Epic monster kill with killer id 0 found, likely Rift Herald killing itself."
                    )
                    continue

                team = game["teams"][
                    "BLUE" if event["killerId"] < 6 else "RED"]

                monster_type = monster_type_dict[event["monsterType"]]

                event_dto = game_dto.LolGameTeamMonsterKill(
                    timestamp=timestamp,
                    type=monster_type,
                    killerId=event["killerId"])

                if monster_type == "DRAGON":
                    try:
                        event_dto["subType"] = monster_subtype_dict[
                            event["monsterSubType"]]
                    # If we don’t know how to translate the monster subtype, we simply leave it as-is
                    except KeyError:
                        event_dto["subType"] = event["monsterSubType"]

                team["monstersKills"].append(event_dto)

            # Buildings kills
            elif event["type"] == "BUILDING_KILL":
                # The teamId here refers to the SIDE of the tower that was killed, so the opponents killed it
                team = game["teams"]["RED" if event["teamId"] ==
                                     100 else "BLUE"]

                # Get the prebuilt "building" event DTO
                event_dto = building_dict[event["position"]["x"],
                                          event["position"]["y"]]

                # Fill its timestamp
                event_dto["timestamp"] = timestamp

                if event.get("killerId"):
                    event_dto["killerId"] = event.get("killerId")

                team["buildingsKills"].append(event_dto)

            # Champions kills
            elif event["type"] == "CHAMPION_KILL":
                position = game_dto.Position(x=event["position"]["x"],
                                             y=event["position"]["y"])

                game["kills"].append(
                    game_dto.LolGameKill(
                        timestamp=timestamp,
                        position=position,
                        killerId=event["killerId"],
                        victimId=event["victimId"],
                        assistsIds=event["assistingParticipantIds"],
                    ))

            # Skill points use
            elif event["type"] == "SKILL_LEVEL_UP":
                player = get_player(game, event["participantId"])

                player["skillsLevelUpEvents"].append(
                    game_dto.LolGamePlayerSkillLevelUpEvent(
                        timestamp=timestamp,
                        slot=event["skillSlot"],
                        type=event["levelUpType"]))

            # Item buying, selling, and undoing
            elif "ITEM" in event["type"]:
                if not event.get("participantId"):
                    riot_transmute_logger.debug(
                        f"Dropping item event because it does not have a participantId:\n{event}"
                    )
                    # Some weird ITEM_DESTROYED events without a participantId can appear in older games (tower items)
                    continue

                player = get_player(game, event["participantId"])
                event_type = event["type"].lstrip("ITEM_")

                if event_type == "UNDO":
                    item_event = game_dto.LolGamePlayerItemEvent(
                        timestamp=timestamp,
                        type=event_type,
                        id=event["afterId"],
                        undoId=event["beforeId"])
                else:
                    item_event = game_dto.LolGamePlayerItemEvent(
                        timestamp=timestamp,
                        type=event_type,
                        id=event["itemId"])

                if add_names:
                    item_event["name"] = lit.get_name(item_event["id"],
                                                      object_type="item")

                player["itemsEvents"].append(item_event)

            # Wards placing and killing
            elif "WARD" in event["type"]:
                if event["type"] == "WARD_KILL":
                    if not event.get("killerId"):
                        riot_transmute_logger.debug(
                            f"Ward kill event without killerId dropped:\n{event}"
                        )
                        continue
                    player = get_player(game, event["killerId"])
                    event_type = "KILLED"
                else:
                    try:
                        player = get_player(game, event["creatorId"])
                    except StopIteration:
                        # TODO Understand events with ward_type=UNDEFINED + creatorId=0, they are dropped atm
                        continue
                    event_type = "PLACED"

                player["wardsEvents"].append(
                    game_dto.LolGamePlayerWardEvent(
                        timestamp=timestamp,
                        type=event_type,
                        wardType=event["wardType"]))

    return game