예제 #1
0
    def __init__(self, known_player_nicks=dict(), teams=TEAM_COLOR, average_precision=2, min_players_per_game=3):
        self.reset()
        self.player_map = AutoCompletePlayerMapAdmin(known_player_nicks)
        self.teams = teams
        self.average_precision = average_precision
        self.min_players_per_game = min_players_per_game

        self.longest_name_length = {
            True: max([len(p) for p in known_player_nicks.keys()] or [DEFAULT_COLUMN_WIDTH]),  # display_bot = True
            False: max(
                [len(p) for p in known_player_nicks.keys() if not self.is_bot(p)] or [DEFAULT_COLUMN_WIDTH]
            ),  # display_bot = False
        }

        self.special_stats = {
            "pweapon": self.get_preffered_weapons,
            "survival_index": self.get_survival_index,
            "cap_index": self.get_cap_index,
            "nemesis": self.get_nemesis,
            "rag_doll": self.get_rag_doll,
            "not_last": self.get_not_last,
        }
예제 #2
0
class NexuizLogParser:
    def __init__(self, known_player_nicks=dict(), teams=TEAM_COLOR, average_precision=2, min_players_per_game=3):
        self.reset()
        self.player_map = AutoCompletePlayerMapAdmin(known_player_nicks)
        self.teams = teams
        self.average_precision = average_precision
        self.min_players_per_game = min_players_per_game

        self.longest_name_length = {
            True: max([len(p) for p in known_player_nicks.keys()] or [DEFAULT_COLUMN_WIDTH]),  # display_bot = True
            False: max(
                [len(p) for p in known_player_nicks.keys() if not self.is_bot(p)] or [DEFAULT_COLUMN_WIDTH]
            ),  # display_bot = False
        }

        self.special_stats = {
            "pweapon": self.get_preffered_weapons,
            "survival_index": self.get_survival_index,
            "cap_index": self.get_cap_index,
            "nemesis": self.get_nemesis,
            "rag_doll": self.get_rag_doll,
            "not_last": self.get_not_last,
        }

    def reset(self):
        """
            Reset variables for a new log file.
        """
        self.in_game = False
        self.count = 0
        self.games = dict()
        self.info = []
        self.total = dict()
        self.average = dict()
        self.logfile_list = []
        self.logline = ""

    def is_bot(self, name):
        return self.player_map.is_bot(name)

    def get_results(self):
        """
            Return a dictionary with all the parsed information.
        """
        return self.games

    def get_total(self):
        """
            Return a dictionary with summary of all games in the log.
        """
        return self.total

    def get_average(self):
        """
            Return a dictionary with summary of all games in the log, in average per game.
        """
        return self.average

    def parse_logs(self, logfile_list):
        """
            Parse the log in `logfile`.
        """
        self.logfile_list = logfile_list
        players_name = dict()
        for logfile in logfile_list:
            line_number = 0
            capture_stack = dict()
            killing_spree = dict()

            for line in open(logfile):
                line_number += 1
                self.logline = "%s:%d :" % (logfile, line_number)

                if (len(line) > 25) and (line[24] == ":"):
                    timestamp = line[3:22]
                    gametime = datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S")

                    command = line[25:].strip().split(":")

                    ########################
                    # COMAND PARSING START #
                    ########################
                    command_name = command[0]
                    if command_name == "gamestart":
                        underscore_pos = command[1].find("_")
                        game_type = command[1][:underscore_pos]
                        map_name = command[1][underscore_pos + 1 :]

                        players_name = dict()
                        capture_stack = dict()
                        killing_spree = dict()
                        self.in_game = True
                        self.games[self.count] = dict()
                        self.games[self.count]["map_data"] = {
                            "map_name": map_name,
                            "start_time": gametime + START_DELAY_TIME,
                            "duration": "",
                            "game_type": game_type,
                        }

                        self.games[self.count]["teams"] = dict()
                        for (t_id, team) in self.teams.items():
                            self.games[self.count]["teams"][team] = {
                                "id": t_id,
                                "color": team,
                                "caps": 0,
                                "score": 0,
                                "last_players": [],
                                "captures_log": [],
                            }

                    elif not self.in_game:
                        # commands outside a game are discarded
                        continue

                    elif command_name == "gameinfo":
                        subcommand = command[1]
                        if subcommand == "end":
                            pass
                        elif subcommand == "mutators":
                            mutators = command[2:]
                        else:
                            # just to discover other gameinfo log lines
                            self.info.append("%s gameinfo subcommand not recognized:" % self.logline)
                            self.info.append(command)

                    elif command_name == "scores":
                        pass

                    elif command_name == "join":
                        # This data is valid only during this game
                        # ip_from: IP address of the player
                        # nick: Nickname of the player
                        # xx: ??
                        player_id = command[1]
                        xx = command[2]
                        ip_from = command[3]
                        nick = ":".join(command[4:])

                        player_name = self.player_map.get_name_from_nick(nick)

                        if "players" not in self.games[self.count]:
                            self.games[self.count]["players"] = dict()

                        if player_id not in players_name:
                            players_name[player_id] = player_name

                        if player_name not in self.games[self.count]["players"]:
                            self.games[self.count]["players"][player_name] = {
                                "id": player_id,
                                "ip": ip_from,
                                "nick": nick,
                                "name": player_name,
                                "team": [],
                                "last_team": "",
                                "kills_by_player": dict(),
                                "deaths_by_player": dict(),
                                "kills_by_weapon": dict(),
                                "killing_spree": [],
                            }
                            for stat in NUMERIC_STATS:
                                self.games[self.count]["players"][player_name][stat] = 0

                            capture_stack[player_name] = []
                            killing_spree[player_name] = 0

                    elif command_name == "team":
                        player_id, team_id = command[1:]
                        change_time = gametime + START_DELAY_TIME - self.games[self.count]["map_data"]["start_time"]
                        if team_id not in self.teams:
                            continue
                        self.games[self.count]["players"][players_name[player_id]]["team"].append(
                            "%s (%s)" % (self.teams[team_id], change_time)
                        )
                        self.games[self.count]["players"][players_name[player_id]]["last_team"] = self.teams[team_id]

                    elif command_name == "kill":
                        text, killer, killed = command[1:4]
                        other_data = command[4:]  # items=killer weapon, victimitems=killed weapon
                        killer = players_name[killer]
                        killed = players_name[killed]
                        self.games[self.count]["players"][killed]["deaths"] += 1

                        killer_weapon, killer_mod = self._parse_weapon(other_data[1][6:], killer, killed)
                        if text in ["frag", "tk"]:
                            killed_weapon, killed_mod = self._parse_weapon(other_data[2][12:], killer, killed)

                        self.games[self.count]["players"][killer]["kills_by_weapon"][killer_weapon] = (
                            self.games[self.count]["players"][killer]["kills_by_weapon"].get(killer_weapon, 0) + 1
                        )

                        if killing_spree[killed] >= N_KILL_SPREE:
                            self.games[self.count]["players"][killed]["killing_spree"].append(killing_spree[killed])
                        killing_spree[killed] = 0

                        if text == "frag":  # kill other player
                            self.games[self.count]["players"][killer]["frags"] += 1
                            self.games[self.count]["players"][killer]["kills_by_player"][killed] = (
                                self.games[self.count]["players"][killer]["kills_by_player"].get(killed, 0) + 1
                            )
                            self.games[self.count]["players"][killed]["deaths_by_player"][killer] = (
                                self.games[self.count]["players"][killed]["deaths_by_player"].get(killer, 0) + 1
                            )

                            if FLAG in killed_mod:
                                self.games[self.count]["players"][killer]["fc_kills"] += 1
                            if SHIELD in killed_mod:
                                self.games[self.count]["players"][killer]["ic_kills"] += 1
                            if STRENGTH in killed_mod:
                                self.games[self.count]["players"][killer]["sc_kills"] += 1
                            if TEXT in killed_mod:
                                self.games[self.count]["players"][killer]["tc_kills"] += 1

                            if FLAG in killer_mod:
                                self.games[self.count]["players"][killer]["kills_wf"] += 1
                            if SHIELD in killer_mod:
                                self.games[self.count]["players"][killer]["kills_wi"] += 1
                            if STRENGTH in killer_mod:
                                self.games[self.count]["players"][killer]["kills_ws"] += 1

                            killing_spree[killer] += 1

                        elif text == "suicide":  # kill himself, by weapon
                            self.games[self.count]["players"][killer]["suicide"] += 1
                        elif text == "accident":  # kill himself, not by weapon
                            self.games[self.count]["players"][killer]["accident"] += 1
                        elif text == "tk":  # TeamMate kill
                            self.games[self.count]["players"][killer]["tk"] += 1
                        else:
                            self.info.append("%s kill text not recognized for command:" % self.logline)
                            self.info.append(command)

                    elif command_name == "ctf":
                        # Recognized subcommand:
                        #  returned --> lost flag automatically returned by timeout
                        #  capture  --> a capture for the team
                        #  return   --> flag returned by a player
                        #  steal    --> flag stealed by a player (flag in the base)
                        #  dropped  --> flag dropped by the player
                        #  pickup   --> flag taken by a player (flag in the field)
                        subcommand = command[1]
                        team_id = command[2]
                        if subcommand == RETURNED:
                            pass
                        elif subcommand in [CAPTURE, RETURN, STEAL, DROPPED, PICKUP]:
                            player_id = command[3]
                            player_name = players_name[player_id]

                            self.games[self.count]["players"][player_name][subcommand] += 1

                            if subcommand in [STEAL, DROPPED, PICKUP]:
                                capture_stack[player_name].append(subcommand)

                            if subcommand == CAPTURE:
                                capture_time = gametime - self.games[self.count]["map_data"]["start_time"]
                                self.games[self.count]["teams"][OPPOSITE_TEAM[team_id]]["captures_log"].append(
                                    (capture_time, player_name)
                                )

                                type_of_capture = capture_stack[player_name][-1]
                                if type_of_capture == STEAL:
                                    self.games[self.count]["players"][player_name]["cap_by_steal"] += 1
                                elif type_of_capture == PICKUP:
                                    self.games[self.count]["players"][player_name]["cap_by_pickup"] += 1

                                capture_stack[player_name] = []

                        else:
                            self.info.append("%s ctf subcommand not recognized:" % self.logline)
                            self.info.append(command)

                    elif command_name == "part":
                        # This means: "player_id left the game"
                        player_id = command[1]
                        self.games[self.count]["players"][players_name[player_id]]["last_team"] = ""

                    elif command_name == "labels":
                        subcommand = command[1]
                        if subcommand == "player":
                            pass
                        elif subcommand == "teamscores":
                            pass
                        else:
                            self.info.append("%s labels subcommand not recognized:" % self.logline)
                            self.info.append(command)

                    elif command_name == "player":
                        # final stats for player
                        player_stats = command[2]
                        player_id = command[5]
                        team_id = command[4]
                        player_score = int(player_stats.split(",")[0])

                        self.games[self.count]["players"][players_name[player_id]]["score"] = player_score

                    elif command_name == "teamscores":
                        # final stats for teams
                        team_stats = command[2]
                        team_id = command[3]
                        if team_id in self.teams and team_stats:
                            team = self.teams[team_id]
                            caps, score = team_stats.split(",")
                            self.games[self.count]["teams"][team]["caps"] = int(caps)
                            self.games[self.count]["teams"][team]["score"] = int(score)

                    elif command_name == "end":
                        self.games[self.count]["map_data"]["end_time"] = gametime
                        self.games[self.count]["map_data"]["duration"] = (
                            gametime - self.games[self.count]["map_data"]["start_time"]
                        )

                        for pname, ks in killing_spree.items():
                            if ks >= N_KILL_SPREE:
                                self.games[self.count]["players"][pname]["killing_spree"].append(ks)

                    elif command_name == "gameover":
                        # This command marks the end of the game
                        self.count += 1
                        self.in_game = False
                        # break # for testing only... I want to play with 1 game's data

                    elif command_name == "vote":
                        # This command shows map voting
                        subcommand = command[1]
                        if subcommand == "keeptwo":
                            pass
                        elif subcommand == "finished":
                            pass

                    elif command_name == "recordset":
                        pass

                    elif command_name == "name":
                        # Player changes his/her name
                        pass

                    else:
                        # This is to show any unknown command
                        self.info.append("%s main command not recognized:" % self.logline)
                        self.info.append(command)

        self._clean_games()
        self._compute_extra_stats()
        self._compute_total()
        self._compute_average()
        self.info += self.player_map.get_info()

    def _clean_games(self):
        for i, game in self.games.items():
            if "players" not in game:
                del self.games[i]
            else:
                players = [p for p in game["players"].keys() if not self.is_bot(p)]
                if len(players) < self.min_players_per_game:
                    del self.games[i]

    def _compute_extra_stats(self):
        for i, game in self.games.items():
            last_player_name = self._get_last_player_name(game["players"])
            self.games[i]["players"][last_player_name]["last"] = 1

            for pname, player in game["players"].items():
                for stat, stat_func in self.special_stats.items():
                    self.games[i]["players"][pname][stat] = stat_func(player)

                if player["last_team"]:
                    self.games[i]["teams"][player["last_team"]]["last_players"].append(pname)

                self.games[i]["players"][pname]["n_killing_spree"] = len(
                    self.games[i]["players"][pname]["killing_spree"]
                )
                self.games[i]["players"][pname]["max_killing_spree"] = max(
                    self.games[i]["players"][pname]["killing_spree"] or [0]
                )

            for tname, team in game["teams"].items():
                self.games[i]["teams"][tname]["last_players"] = ", ".join(self.games[i]["teams"][tname]["last_players"])

    def _compute_total(self):
        for game in self.games.values():
            if "players" not in game:
                continue

            for player in game["players"].values():
                pname = player["name"]
                if pname not in self.total:
                    self.total[pname] = {"name": pname, "team": [], "kills_by_weapon": dict(), "games_played": 0}

                self.total[pname]["games_played"] += 1

                for stat in NUMERIC_STATS:
                    self.total[pname][stat] = self.total[pname].get(stat, 0) + player[stat]

                for stat in STATS_BY_PLAYER:
                    if stat not in self.total[pname]:
                        self.total[pname][stat] = dict()

                    for p2name, num in player[stat].items():
                        self.total[pname][stat][p2name] = self.total[pname][stat].get(p2name, 0) + num

                for weapon, num in player["kills_by_weapon"].items():
                    self.total[pname]["kills_by_weapon"][weapon] = (
                        self.total[pname]["kills_by_weapon"].get(weapon, 0) + num
                    )

                self.total[pname]["pweapon"] = self.get_preffered_weapons(self.total[pname], 3)
                self.total[pname]["survival_index"] = self.get_survival_index(self.total[pname])
                self.total[pname]["cap_index"] = self.get_cap_index(self.total[pname])
                self.total[pname]["nemesis"] = self.get_nemesis(self.total[pname])
                self.total[pname]["rag_doll"] = self.get_rag_doll(self.total[pname])

                self.total[pname]["max_killing_spree"] = max(
                    self.total[pname].get("max_killing_spree", 0), player["max_killing_spree"]
                )
                self.total[pname]["max_killing_spree_sum"] = (
                    self.total[pname].get("max_killing_spree_sum", 0) + player["max_killing_spree"]
                )

    def _compute_average(self):
        stats_by_something = STATS_BY_PLAYER + STATS_BY_WEAPON

        def av(val, tot):
            return round(val * 1.0 / tot, self.average_precision)

        for pname, player in self.total.items():
            num = player["games_played"]
            self.average[pname] = {"name": pname, "team": [], "games_played": num}

            for stat in NUMERIC_STATS:
                self.average[pname][stat] = av(player[stat], num)

            for stat in stats_by_something:
                self.average[pname][stat] = dict()
                for key, val in player[stat].items():
                    self.average[pname][stat][key] = av(val, num)

            for stat, stat_func in self.special_stats.items():
                self.average[pname][stat] = stat_func(self.average[pname])

            self.average[pname]["max_killing_spree"] = av(player["max_killing_spree_sum"], num)

    def _parse_weapon(self, weapon, killer="", killed=""):
        weapon_id = ""
        weapon_mod = ""
        for c in weapon:
            try:
                int(c)
                weapon_id += c
            except ValueError as e:
                weapon_mod += c

        weapon_id = int(weapon_id)
        if weapon_id not in WEAPONS:
            self.info.append(
                "%s Unknown weapon id: %s, map: %s (%s -> %s)"
                % (self.logline, weapon, self.games[self.count]["map_data"]["map_name"], killer, killed)
            )
            weapon_str = "new weapon"
        else:
            weapon_str = WEAPONS[weapon_id]

        clean_mod = ""
        for m in weapon_mod:
            if m not in WEAPON_MOD:
                self.info.append(
                    "%s Unknown weapon mod: %s, map: %s (%s -> %s)"
                    % (self.logline, weapon, self.games[self.count]["map_data"]["map_name"], killer, killed)
                )
            else:
                clean_mod += m

        return (weapon_str, clean_mod)

    def get_preffered_weapons(self, player, num=2):
        weapons = sorted(player["kills_by_weapon"].items(), key=lambda x: x[1], reverse=True)[:num]
        return ", ".join(["%s(%d)" % w for w in weapons])

    def get_survival_index(self, player):
        return round((player["frags"] * 1.0) / (player["deaths"] or 1), 2)

    def get_cap_index(self, player):
        return round((player["cap_by_steal"] * 100.0) / (player["steal"] or 1), 1)

    def get_nemesis(self, player):
        ordered_nemesis = sorted(player["deaths_by_player"].items(), key=lambda x: x[1], reverse=True)
        try:
            return "%s(%d)" % ordered_nemesis[0]
        except IndexError as e:
            return ""

    def get_rag_doll(self, player):
        ordered_rag_doll = sorted(player["kills_by_player"].items(), key=lambda x: x[1], reverse=True)
        try:
            return "%s(%d)" % ordered_rag_doll[0]
        except IndexError as e:
            return ""

    def get_not_last(self, player, all_games=False):
        if all_games:
            return len(self.games) - player["last"]
        else:
            return 1 - player["last"]

    def get_total_time_played(self):
        durations = [game["map_data"]["duration"] for i, game in self.games.items()]
        return sum(durations, AVERAGE_TIME_TO_SELECT_A_MAP * len(self.games))

    def display_parser_info(self):
        """
            Display info messages generated by the parser.
        """
        if not self.info:
            return
        print "\nPARSER INFO:"
        for e in self.info:
            print e

    def _sorted_games(self):
        return sorted(self.games.items(), key=lambda x: x[0])

    def _game_is_ctf(self, game):
        return "teams" in game and game["map_data"]["game_type"] == "ctf"

    def _filter_players_from_bots(self, players, display_bot=False):
        def valid(player):
            if self.is_bot(player["name"]) and not display_bot:
                return False
            return True

        return [p for p in players.values() if valid(p)]

    def _filter_players_and_sort_by_stat(self, players, display_bot=False, stat="name"):
        filtered_players = self._filter_players_from_bots(players, display_bot)
        return sorted(filtered_players, key=itemgetter(stat))

    def _get_last_player_name(self, players):
        filtered_players = self._filter_players_from_bots(players, display_bot=False)
        filtered_players.sort(key=itemgetter("deaths"), reverse=True)
        filtered_players.sort(key=itemgetter("score"))
        return filtered_players[0]["name"]

    def _output_players_scores(self, render, players, table="game"):
        header = {"total": render.total_table_header, "game": render.game_table_header}
        row = {"total": render.total_table_row, "game": render.game_table_row}

        output = header[table]()
        for player in players:
            player["teams"] = ", ".join(player["team"])
            output += row[table](player)
        return output

    def _output_kills_by_player(self, render, players):
        output = render.kills_by_player_table_header([p["name"] for p in players])
        for killer in players:
            line = [killer["name"]]
            for killed in players:
                line.append(killer["kills_by_player"].get(killed["name"], 0))
            output += render.kills_by_player_table_row(line)
        return output

    def _output_teams_scores(self, render, teams):
        output = render.teams_table_header()
        for team in teams.values():
            team["captures"] = ", ".join(["%s(%s)" % cl for cl in team["captures_log"]])
            output += render.teams_table_row(team)
        return output

    def output(self, output="html", display_bot=False, display_parcial=True, display_total=True):
        """
            Outputs the results of the parsed log file.
            @display_bot: if set, display bot results.
            @display_parcial: if set, show info of all games
            @display_total: if set, show summary info
        """
        options = {"html": HTMLRender, "txt": PlainTextRender}
        if output not in options:
            output = "html"
        render = options[output](header_names=HEADER_NAMES, lnl=self.longest_name_length[display_bot])

        content = {
            "title": "Nexuiz Statistics",
            "logfiles": ", ".join([os.path.basename(log) for log in self.logfile_list]),
            "total_table": "",
            "games_tables": "",
        }

        if display_parcial:
            for i, game in self._sorted_games():
                players = self._filter_players_and_sort_by_stat(game["players"], display_bot)
                game_data = game["map_data"]
                game_data["player_stats"] = self._output_players_scores(render, players, table="game")
                game_data["player_vs_player"] = self._output_kills_by_player(render, players)

                if self._game_is_ctf(game):
                    game_data["teams_stats"] = self._output_teams_scores(render, game["teams"])
                else:
                    game_data["teams_stats"] = ""

                content["games_tables"] += render.game(game_data)

        if display_total:
            total_players = self._filter_players_and_sort_by_stat(self.total, display_bot)
            average_players = self._filter_players_and_sort_by_stat(self.average, display_bot)
            total_data = {
                "game_number": len(self.games),
                "time_played": self.get_total_time_played(),
                "player_stats": self._output_players_scores(render, total_players, table="total"),
                "average_stats": self._output_players_scores(render, average_players, table="total"),
                "player_vs_player": self._output_kills_by_player(render, total_players),
            }
            content["total_table"] = render.total(total_data)

        return render.base(content)