diff --git a/analyze_wotb_replays.py b/analyze_wotb_replays.py index d6bc638..49f901e 100644 --- a/analyze_wotb_replays.py +++ b/analyze_wotb_replays.py @@ -40,7 +40,8 @@ replay_summary_flds = [ 'map_name', 'battle_duration', 'protagonist', - 'title' + 'title', + 'mastery_badge' ] replay_details_flds = [ @@ -115,12 +116,16 @@ class BattleRecordCategory(): RESULT_CAT_FRMT = '{:>17s}' @classmethod - def get_result_categories(cls, extended_stats: bool = False): - if extended_stats: - return cls._result_categories_default + cls._result_categories_extended + def get_result_categories(cls, extended_cats: list = None): + if extended_cats != None: + return cls._result_categories_default + extended_cats else: return cls._result_categories_default + @classmethod + def get_extra_categories(cls) -> list: + return cls._result_categories_extended + def __init__(self, cat_name : str): self.category_name = cat_name self.category = collections.defaultdict(def_value_BattleRecord) @@ -137,6 +142,7 @@ class BattleRecordCategory(): def get_sub_categories(self): return self.category.keys() + def record_result(self, result: dict): try: cat = None @@ -161,7 +167,8 @@ class BattleRecordCategory(): def print_results(self): try: - print(' '.join(self.get_headers())) + first_btl_record = list(self.category.values())[0] + print(' '.join(first_btl_record.get_headers(self.category_name))) for row in self.get_results(): print(' : '.join(row)) except KeyError as err: @@ -170,6 +177,7 @@ class BattleRecordCategory(): bu.error(str(err)) return None + def get_results(self): try: results = [] @@ -210,9 +218,13 @@ class BattleRecord(): 'allies_wins' : [ 'Allies WR', 'Average WR of allies at the tier of their tank', 9, '{:9.2%}' ], 'enemies_wins' : [ 'Enemies WR', 'Average WR of enemies at the tier of their tank', 10, '{:10.2%}' ], 'allies_battles' : [ 'Allies Btls', 'Average number battles of the allies', 11, '{:11.0f}' ], - 'enemies_battles' : [ 'Enemies Btls', 'Average number battles of the enemies', 12, '{:12.0f}' ] + 'enemies_battles' : [ 'Enemies Btls', 'Average number battles of the enemies', 12, '{:12.0f}' ], + 'player_wins' : [ 'Player WR', 'Average WR of the player', 9, '{:9.2%}' ], + 'player_battles' : [ 'Player Btls', 'Average number battles of the allies', 11, '{:11.0f}' ] } + _team_fields = [ 'wins', 'battles' ] + # fields to display in results _result_fields_default = [ 'battles', @@ -220,6 +232,7 @@ class BattleRecord(): 'damage_made', 'enemies_spotted', 'top_tier', + 'player_wins', 'allies_wins', 'enemies_wins', 'allies_battles', @@ -244,50 +257,65 @@ class BattleRecord(): 'dmg_block_rate' : [ 'damage_blocked', 'damage_received' ] } + _extended_stats = False + result_fields = list() + avg_fields = set() + ratio_fields = set() + + RESULT_CAT_HEADER_FRMT = '{:_<17s}' @classmethod - def get_result_fields(cls, extended_stats: bool = False): - if extended_stats: - return cls._result_fields_default + cls._result_fields_extended - else: - return cls._result_fields_default + def set_fields(cls, extended_stats: bool = False): + try: + cls._extended_stats = extended_stats + if cls._extended_stats: + cls.result_fields = cls._result_fields_default + cls._result_fields_extended + else: + cls.result_fields = cls._result_fields_default + cls.result_fields_ratio = set(cls._result_ratios.keys()) & set(cls.result_fields) + + cls.avg_fields = set(cls.result_fields) + for field in ['battles', MISSING_STATS]: + if field in cls.avg_fields: + cls.avg_fields.remove(field) + cls.avg_fields.difference_update(set(cls._result_ratios.keys())) + for ratio in cls.result_fields_ratio: + cls.ratio_fields.add(cls._result_ratios[ratio][0]) + cls.ratio_fields.add(cls._result_ratios[ratio][1]) + except Exception as err: + bu.error('BattleRecord:' ,exception=err) + + + @classmethod + def get_result_fields(cls) -> set: + return cls.result_fields - @classmethod def get_fields_avg(cls) -> set: - avg_fields = set(cls.get_result_fields(True)) - avg_fields.remove('battles') - avg_fields.remove(MISSING_STATS) - avg_fields.difference_update(cls._result_ratios.keys()) - return avg_fields - + return cls.avg_fields @classmethod def get_fields_ratio(cls) -> set: - ratio_fields = set() - for ratio in cls._result_ratios.keys(): - ratio_fields.add(cls._result_ratios[ratio][0]) - ratio_fields.add(cls._result_ratios[ratio][1]) - return ratio_fields + return cls.ratio_fields + + @classmethod + def get_team_fields(cls) -> list: + return cls._team_fields - def __init__(self, extended_stats: bool = False): + def __init__(self): try: + self.battles = 0 self.missing_stats = 0 self.n_players = 0 self.ratios_ready = False self.results_ready = False self.results = collections.defaultdict(def_value_zero) - self.avg_fields = self.get_fields_avg() - self.ratio_fields = self.get_fields_ratio() - self.result_fields = self.get_result_fields(extended_stats) - self.result_fields_ratio = set(self._result_ratios.keys()) & set(self.get_result_fields(extended_stats)) - except KeyError as err: bu.error('Key not found', err) @@ -297,7 +325,7 @@ class BattleRecord(): for field in self.avg_fields | self.ratio_fields: if (field in result) and (result[field] != None): self.results[field] += result[field] - self.battles += 1 + self.battles += 1 self.n_players += result[N_PLAYERS] self.missing_stats += result[MISSING_STATS] return True @@ -463,6 +491,7 @@ async def main(argv): parser.add_argument('-id', dest='account_id', type=int, default=WG_ID, help='WG account_id to analyze') parser.add_argument('-a', '--account', type=str, default=None, help='WG account nameto analyze. Format: ACCOUNT_NAME@SERVER') parser.add_argument('-x', '--extended', action='store_true', default=False, help='Print Extended stats') + parser.add_argument('-X', '--extra_categories', choices=BattleRecordCategory.get_extra_categories(), default=None, nargs='*', help='Print Extended categories') parser.add_argument('--hist', action='store_true', default=False, help='Print player histograms (WR/battles)') parser.add_argument('--stat_func', default='tank_tier', choices=STAT_FUNC.keys(), help='Select how to calculate for ally/enemy performance: tank-tier stats, global player stats') parser.add_argument('-u', '--url', action='store_true', default=False, help='Print replay URLs') @@ -487,6 +516,8 @@ async def main(argv): args.account_id = await wg.get_account_id(args.account) bu.debug('WG account_id: ' + str(args.account_id)) + BattleRecord.set_fields(args.extended) + #### Connect to MongoDB (TBD) bu.debug('DB_SERVER: ' + DB_SERVER) bu.debug('DB_PORT: ' + str(DB_PORT)) @@ -544,7 +575,7 @@ async def main(argv): teamresults = calc_team_stats(results, player_stats, stat_id_map, args) process_battle_results(teamresults, args) if args.hist: - print('\nPlayer Historgrams______', end='', flush=True) + print('\nPlayer Histograms______', end='', flush=True) process_player_dist(results, player_stats, stat_id_map) bu.debug('Finished. Cleaning up..................') except Exception as err: @@ -588,9 +619,7 @@ def process_battle_results(results: dict, args : argparse.Namespace): urls = collections.OrderedDict() categories = {} - cats = result_categories_default - if args.extended: - cats = result_categories.keys() + cats = BattleRecordCategory.get_result_categories(args.extra_categories) for cat in cats: categories[cat] = BattleRecordCategory(cat) @@ -619,50 +648,54 @@ def process_battle_results(results: dict, args : argparse.Namespace): async def process_player_stats(players, N_workers: int, args : argparse.Namespace, db : motor.motor_asyncio.AsyncIOMotorDatabase) -> dict: """Start stat_workers to retrieve and store players' stats""" ## Create queue of player-tank pairs to find stats for - statsQ = asyncio.Queue() - bu.debug('Create player stats queue: ' + str(len(players)) + ' players') - stat_id_map = {} - stat_ids = set() + try: + statsQ = asyncio.Queue() + bu.debug('Create player stats queue: ' + str(len(players)) + ' players') + stat_id_map = {} + stat_ids = set() - bu.set_progress_bar('Fetching player stats', len(players), 25, True) + bu.set_progress_bar('Fetching player stats', len(players), 25, True) - stat_id_map_func = globals()[STAT_FUNC[args.stat_func][0]] + stat_id_map_func = globals()[STAT_FUNC[args.stat_func][0]] - for player in players: - stat_id_map[player] = stat_id_map_func(player) - stat_ids.add(stat_id_map[player]) + for player in players: + stat_id_map[player] = stat_id_map_func(player) + stat_ids.add(stat_id_map[player]) + + # create statsQ + for stat_id in stat_ids: + await statsQ.put(stat_id) + + # Process player WR / battle stats + stats_tasks = [] + bu.debug('Starting player stats workers') + for i in range(N_workers): + bu.debug("Starting worker " + str(i)) + stats_tasks.append(asyncio.create_task(stat_worker(statsQ, i, args, db))) - # create statsQ - for stat_id in stat_ids: - await statsQ.put(stat_id) + bu.debug('Waiting stats workers to finish') + await statsQ.join() + bu.debug('Cancelling stats workers') + for task in stats_tasks: + task.cancel() + player_stats = {} + stat_id_remap = {} - # Process player WR / battle stats - stats_tasks = [] - bu.debug('Starting player stats workers') - for i in range(N_workers): - bu.debug("Starting worker " + str(i)) - stats_tasks.append(asyncio.create_task(stat_worker(statsQ, i, args, db))) - - bu.debug('Waiting stats workers to finish') - await statsQ.join() - bu.debug('Cancelling stats workers') - for task in stats_tasks: - task.cancel() - player_stats = {} - stat_id_remap = {} + bu.debug('Gathering stats worker outputs') + for (stats, id_remap) in await asyncio.gather(*stats_tasks): + player_stats = {**player_stats, **stats} + stat_id_remap = {**stat_id_remap, **id_remap} - bu.debug('Gathering stats worker outputs') - for (stats, id_remap) in await asyncio.gather(*stats_tasks): - player_stats = {**player_stats, **stats} - stat_id_remap = {**stat_id_remap, **id_remap} + bu.finish_progress_bar() + + ## DO REMAPPING + stat_id_map = remap_stat_id(stat_id_map, stat_id_remap) - bu.finish_progress_bar() - - ## DO REMAPPING - stat_id_map = remap_stat_id(stat_id_map, stat_id_remap) - - bu.debug('Returning player_stats') - return (player_stats, stat_id_map) + bu.debug('Returning player_stats') + return (player_stats, stat_id_map) + except Exception as err: + bu.error(exception=err) + return None def remap_stat_id(stat_id_map: dict, stat_id_remap: dict): @@ -682,14 +715,8 @@ def calc_team_stats(results: list, player_stats : dict, stat_id_map : dict, arg ## Bug here?? #stat_types = player_stats[list(player_stats.keys())[0]].keys() stat_types = list() - p_team_fields = re.compile('^allies_(.+)$') - for field in result_fields: - m = p_team_fields.match(field) - if m != None: - stat_types.append(m.group(1)) - - bu.debug('stat_types: ' + ','.join(stat_types)) - + stat_types = BattleRecord.get_team_fields() + for result in results: try: missing_stats = 0 @@ -697,6 +724,8 @@ def calc_team_stats(results: list, player_stats : dict, stat_id_map : dict, arg n_allies = collections.defaultdict(def_value_zero) allies_stats = collections.defaultdict(def_value_zero) #bu.debug('Processing Allies') + + for ally in result['allies']: # Player itself is not in 'allies': see read_replay_JSON() ally_mapped = stat_id_map[ally] @@ -708,13 +737,6 @@ def calc_team_stats(results: list, player_stats : dict, stat_id_map : dict, arg allies_stats[stat] += player_stats[ally_mapped][stat] n_allies[stat] += 1 - #bu.debug('Processing Allies\' avg stats') - for stat in stat_types: - if n_allies[stat] > 0: - result['allies_' + str(stat)] = allies_stats[stat] / n_allies[stat] - else: - bu.debug('No allies stats for: ' + str(result)) - #bu.debug('Processing Enemies') n_enemies = collections.defaultdict(def_value_zero) enemies_stats = collections.defaultdict(def_value_zero) @@ -728,12 +750,23 @@ def calc_team_stats(results: list, player_stats : dict, stat_id_map : dict, arg enemies_stats[stat] += player_stats[enemy_mapped][stat] n_enemies[stat] += 1 - #bu.debug('Processing Enemies\' avg stats') + #bu.debug('Processing avg stats') + player_mapped = stat_id_map[result['player']] + for stat in stat_types: + if n_allies[stat] > 0: + result['allies_' + str(stat)] = allies_stats[stat] / n_allies[stat] + else: + bu.debug('No allies stats for: ' + str(result)) + if n_enemies[stat] > 0: result['enemies_' + str(stat)] = enemies_stats[stat] / n_enemies[stat] else: - bu.debug('No allies stats for: ' + str(result)) + bu.debug('No enemies stats for: ' + str(result)) + + if player_stats[player_mapped][stat] != None: + result['player_' + str(stat)] = player_stats[player_mapped][stat] + result[N_PLAYERS] = n_players result[MISSING_STATS] = missing_stats return_list.append(result) @@ -1067,6 +1100,7 @@ async def replay_reader(queue: asyncio.Queue, readerID: int, args : argparse.Nam # if (account_id != None): playerstanks.update(set(result['allies'])) playerstanks.update(set(result['enemies'])) + playerstanks.update(set([ result['player'] ])) results.append(result) bu.debug('Marking task ' + str(replayID) + ' done') @@ -1090,9 +1124,7 @@ async def read_replay_JSON(replay_json: dict, args : argparse.Namespace) -> dict try: result['battle_start_timestamp'] = int(replay_json['data']['summary']['battle_start_timestamp']) # TBD... - # protagonist = int(replay_json['data']['summary']['protagonist']) - # protagonist_tank = int() - # result['protagonist'] = get_stat_id(protagonist, protagonist_tank, result['battle_start_timestamp']) + protagonist = int(replay_json['data']['summary']['protagonist']) if account_id == None: account_id = replay_json['data']['summary']['protagonist'] @@ -1120,11 +1152,15 @@ async def read_replay_JSON(replay_json: dict, args : argparse.Namespace) -> dict result['enemies'] = set() btl_duration = 0 btl_tier = 0 + protagonist_tank = None for player in replay_json['data']['summary']['details']: btl_duration = max(btl_duration, player['time_alive']) player_tank_tier = wg.get_tank_data(player['vehicle_descr'], 'tier') btl_tier = max(btl_tier, player_tank_tier) + if (protagonist != None) and (player['dbid'] == protagonist): + protagonist_tank = player['vehicle_descr'] + if player['dbid'] == account_id: # player itself is not put in results['allies'] tmp = {} @@ -1153,7 +1189,10 @@ async def read_replay_JSON(replay_json: dict, args : argparse.Namespace) -> dict result['allies'].add(get_stat_id(tmp_account_id, tmp_tank_id, tmp_battletime)) else: result['enemies'].add(get_stat_id(tmp_account_id, tmp_tank_id, tmp_battletime)) - + + ## Rather use 'player' than incomprehensible 'protagonist'... + result['player'] = get_stat_id(protagonist, protagonist_tank, result['battle_start_timestamp']) + bu.debug('Player stats_id: ' + result['player']) # remove platoon buddy from stats if result['squad_index'] != None: for player in replay_json['data']['summary']['details']: @@ -1200,17 +1239,27 @@ def get_stat_id(account_id: int, tank_id: int, battletime: int) -> str: def get_stat_id_tank_tier(stat_id_str: str) -> str: """Return stat_id = account_id:tank_tier""" - stat_id = stat_id_str.split(':') - tank_tier = wg.get_tank_tier(int(stat_id[1])) - battle_time = (int(stat_id[2]) // BATTLE_TIME_BUCKET) * BATTLE_TIME_BUCKET - return ':'.join([ stat_id[0], str(tank_tier), str(battle_time) ]) + try: + stat_id = stat_id_str.split(':') + tank_tier = wg.get_tank_tier(int(stat_id[1])) + battle_time = (int(stat_id[2]) // BATTLE_TIME_BUCKET) * BATTLE_TIME_BUCKET + return ':'.join([ stat_id[0], str(tank_tier), str(battle_time) ]) + except Exception as err: + bu.error('Stats_id: ' + stat_id_str) + bu.error(exception=err) + return None def get_stat_id_player(stat_id_str: str) -> str: """Return stat_id = account_id:tank_tier""" - stat_id = stat_id_str.split(':') - battle_time = (int(stat_id[2]) // BATTLE_TIME_BUCKET) * BATTLE_TIME_BUCKET - return ':'.join([ stat_id[0], str(battle_time) ]) + try: + stat_id = stat_id_str.split(':') + battle_time = (int(stat_id[2]) // BATTLE_TIME_BUCKET) * BATTLE_TIME_BUCKET + return ':'.join([ stat_id[0], str(battle_time) ]) + except Exception as err: + bu.error('Stats_id: ' + stat_id_str) + bu.error(exception=err) + return None ### main()