Files
blitz-tools/analyze_wotb_replays.py
T

1073 lines
36 KiB
Python
Raw Normal View History

2018-10-08 00:14:56 +08:00
#!/usr/bin/python3.7
# Script Analyze WoT Blitz replays
import sys, argparse, json, os, concurrent, inspect, aiohttp, asyncio, aiofiles, aioconsole
import motor.motor_asyncio, configparser, ssl, re, logging, time, xmltodict, collections
from datetime import datetime, timedelta, date
2018-10-08 00:14:56 +08:00
import blitzutils as bu
from blitzutils import WG
2018-11-05 14:48:07 +02:00
from blitzutils import WoTinspector
2018-10-08 00:14:56 +08:00
logging.getLogger("asyncio").setLevel(logging.DEBUG)
WG_APP_ID = '81381d3f45fa4aa75b78a7198eb216ad'
FILE_CONFIG = 'blitzstats.ini'
DB_C_TANK_STATS = 'WG_TankStats'
DB_C_PLAYER_STATS = 'WG_PlayerStats'
2018-10-08 00:14:56 +08:00
wg = None
2018-11-05 14:48:07 +02:00
wi = None
2018-10-08 00:14:56 +08:00
REPLAY_N = 0
2019-08-17 00:50:48 +03:00
REPLAY_I = 0
2018-10-08 00:14:56 +08:00
STAT_TANK_BATTLE_MIN = 100
## For different player stat functions (global stats, tank tier stats, etc)
# 1st function = for forming stat_id, 2nd for DB stats query, 3rd for WG API stats query
STAT_FUNC = {
'tank_tier': [ 'get_stat_id_tank_tier', 'get_db_tank_tier_stats', 'get_wg_tank_tier_stats' ],
'player': [ 'get_stat_id_player', 'get_db_player_stats', 'get_wg_player_stats' ]
}
2018-10-08 00:14:56 +08:00
replay_summary_flds = [
'battle_result',
'battle_type',
'map_name',
'battle_duration',
'protagonist',
'title'
2018-10-08 00:14:56 +08:00
]
replay_details_flds = [
'damage_made',
'damage_assisted_track',
'damage_assisted',
'damage_received',
'damage_blocked',
'enemies_damaged',
'enemies_destroyed',
'hitpoints_left',
'time_alive',
'distance_travelled',
'base_capture_points',
'base_defend_points',
'wp_points_earned',
'wp_points_stolen',
'hits_received',
'hits_bounced',
'hits_splash',
'hits_pen',
'shots_made',
'shots_hit',
'shots_pen',
'shots_splash',
'enemies_spotted'
]
result_ratios = {
'KDR' : [ 'enemies_destroyed', 'destroyed' ],
'DR' : [ 'damage_made', 'damage_received' ],
'hit_rate' : [ 'shots_hit', 'shots_made' ],
'pen_rate' : [ 'shots_pen', 'shots_hit' ],
'dmg_block_rate' : [ 'damage_blocked', 'damage_received' ]
}
result_categories = {
2018-12-14 23:33:38 +02:00
'total' : [ 'TOTAL', 'total' ],
2018-10-08 00:44:39 +08:00
'battle_result' : [ 'Result', [ 'Loss', 'Win', 'Draw']],
2018-10-08 00:14:56 +08:00
'battle_type' : [ 'Battle Type',['Encounter', 'Supremacy']],
2018-12-14 23:33:38 +02:00
'tank_tier' : [ 'Tank Tier', 'number' ],
2018-10-08 00:14:56 +08:00
'top_tier' : [ 'Tier', ['Bottom tier', 'Top tier']],
2018-12-14 23:33:38 +02:00
'tank_name' : [ 'Tank', 'string' ],
2019-08-17 00:50:48 +03:00
'map_name' : [ 'Map', 'string' ],
'battle_i' : [ 'Battle #', 'number']
2018-10-08 00:14:56 +08:00
}
2019-08-17 00:50:48 +03:00
result_categories_default = [
'total',
'battle_result',
2020-02-08 12:07:26 +02:00
'tank_tier',
'top_tier',
'tank_name',
'map_name'
2019-08-17 00:50:48 +03:00
]
RESULT_CAT_HEADER_FRMT = '{:_<17s}'
RESULT_CAT_FRMT = '{:>17s}'
2018-10-08 00:14:56 +08:00
## Syntax: Check how the replay JSON files look. The algorithm is counting/recording fields
2018-10-08 00:14:56 +08:00
result_fields = {
'battles' : [ 'Battles', 'Number of battles', 8, '{:^8.0f}' ],
2018-12-15 14:12:12 +02:00
'win' : [ 'WR', 'Win rate', 6, '{:6.1%}' ],
'damage_made' : [ 'DPB', 'Average Damage', 5, '{:5.0f}' ],
'DR' : [ 'DR', 'Damage Ratio', 5, '{:5.1f}' ],
'KDR' : [ 'KDR', 'Kills / Death', 4, '{:4.1f}' ],
2018-10-08 00:14:56 +08:00
'enemies_spotted' : [ 'Spot', 'Enemies spotted per battle', 4, '{:4.1f}' ],
'hit_rate' : [ 'Hit rate', 'Shots hit / all shots made', 8, '{:8.1%}' ],
'pen_rate' : [ 'Pen rate', 'Shots pen / shots hit', 8, '{:8.1%}' ],
2018-12-15 14:12:12 +02:00
'survived' : [ 'Surv%', 'Survival rate', 6, '{:6.1%}' ],
2018-10-08 00:14:56 +08:00
'time_alive%' : [ 'T alive%', 'Percentage of time being alive in a battle', 8, '{:8.0%}' ],
2018-12-15 14:12:12 +02:00
'top_tier' : [ 'Top tier', 'Share of games as top tier', 8, '{:8.0%}' ],
'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%}' ],
2018-10-08 00:14:56 +08:00
'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}' ]
2018-10-08 00:14:56 +08:00
}
## Syntax: key == stat field in https://api.wotblitz.eu/wotb/tanks/stats/ (all.KEY)
## Value array [ 'Stat Title', [ 0, data buckets ....], scaling_factor_for_bucket_values, 'print_format' ]
histogram_fields = {
'wins' : [ 'Win rate', [0, .35, .40, .45, .5, .55, .60, .65, 1], 100, '{:.0f}%' ],
'damage_dealt' : [ 'Avg. Dmg.', [0, .25e3, .5e3, 1e3, 1.5e3, 2e3,2.5e3,3e3,100e3], 1, '{:.0f}' ],
'battles' : [ 'Battles', [0, 1e3, 3e3, 5e3, 10e3, 15e3, 25e3, 50e3, 5e7], .001, '{:.0f}k'] # battles is a mandatory stat to include
2018-10-08 00:14:56 +08:00
}
def def_value_zero():
2018-10-08 00:14:56 +08:00
return 0
def def_value_BattleRecord():
2018-10-08 00:14:56 +08:00
return BattleRecord()
class PlayerHistogram():
def __init__(self, field: str, name: str, fields : list, factor: float, format: str ):
self.field = field
self.name = name
self.fields = fields
self.cat_factor = factor
self.cat_format = format
self.ncat = len(self.fields) - 1
self.allies = [0] * self.ncat
self.enemies = [0] * self.ncat
self.total = [0] * self.ncat
def record_ally(self, stat: float):
for cat in range(0, self.ncat):
if self.fields[cat] <= stat <= self.fields[cat+1]:
self.allies[cat] += 1
self.total[cat] += 1
return True
return False
def record_enemy(self, stat: float):
for cat in range(0, self.ncat):
if self.fields[cat] <= stat <= self.fields[cat+1]:
self.enemies[cat] += 1
self.total[cat] += 1
return True
return False
def get_category_name(self, cat: int):
if cat < self.ncat - 1:
return self.format_category(cat) + ' - ' + self.format_category(cat+1)
else:
return self.format_category(cat) + ' -'
def format_category(self, cat: int):
val = self.fields[cat]
return self.cat_format.format(float(val * self.cat_factor))
def print(self):
N_enemies = 0
N_allies = 0
for cat in range(0, self.ncat):
N_enemies += self.enemies[cat]
N_allies += self.allies[cat]
N_total = N_allies + N_enemies
print("\n{:12s} | {:13s} | {:13s} | {:13s}".format(self.name, "Allies", "Enemies", "TOTAL"))
for cat in range(0, self.ncat):
print("{:12s} | {:5d} ({:4.1f}%) | {:5d} ({:4.1f}%) | {:5d} ({:4.1f}%)".format(self.get_category_name(cat), self.allies[cat], self.allies[cat]/N_allies*100, self.enemies[cat], self.enemies[cat]/N_enemies*100, self.allies[cat] + self.enemies[cat], (self.allies[cat] + self.enemies[cat])/N_total*100 ))
return None
2018-10-08 00:14:56 +08:00
class BattleRecord():
def __init__(self):
try:
self.battles = 0
self.ratios_ready = False
self.results_ready = False
self.results = collections.defaultdict(def_value_zero)
2018-10-08 00:14:56 +08:00
self.avg_fields = set(result_fields.keys())
self.avg_fields.remove('battles')
self.avg_fields.difference_update(result_ratios.keys())
self.ratio_fields = set()
for ratio in result_ratios.keys():
self.ratio_fields.add(result_ratios[ratio][0])
self.ratio_fields.add(result_ratios[ratio][1])
except KeyError as err:
bu.error('Key not found', err)
2018-10-08 00:14:56 +08:00
def record_result(self, result : dict) -> bool:
2018-10-08 00:14:56 +08:00
try:
for field in self.avg_fields | self.ratio_fields:
if result[field] != None:
self.results[field] += result[field]
self.battles += 1
return True
except KeyError as err:
bu.error('Key not found', err)
2018-10-08 00:14:56 +08:00
return False
def calc_ratios(self) -> bool:
2018-10-08 00:14:56 +08:00
if self.ratios_ready:
return True
try:
ratio_result_flds = set(result_ratios.keys()) & set(result_fields.keys())
for field in ratio_result_flds:
if self.results[result_ratios[field][1]] != 0:
self.results[field] = self.results[result_ratios[field][0]] / self.results[result_ratios[field][1]]
else:
self.results[field] = float('Inf')
self.ratios_ready = True
return True
except KeyError as err:
bu.error('Key not found', err)
2018-10-08 00:14:56 +08:00
return False
def calc_results(self):
2018-10-08 00:14:56 +08:00
if self.results_ready == True:
return True
if not self.ratios_ready:
self.calc_ratios()
2018-10-08 00:14:56 +08:00
try:
for field in self.avg_fields:
2019-08-17 01:16:58 +03:00
self.results[field] = self.results[field] / max(self.battles,1)
2018-10-08 00:14:56 +08:00
self.results['battles'] = self.battles
self.results_ready = True
return True
except KeyError as err:
bu.error('Key not found', err)
2018-10-08 00:14:56 +08:00
except Exception as err:
bu.error(exception=err)
2018-10-08 00:14:56 +08:00
return False
def get_results(self):
2018-10-08 00:14:56 +08:00
if not self.results_ready:
self.calc_results()
2018-10-08 00:14:56 +08:00
results = []
try:
for field in result_fields.keys():
results.append( result_fields[field][3].format(self.results[field]) )
return results
except KeyError as err:
bu.error('Key not found', err)
2018-10-08 00:14:56 +08:00
except Exception as err:
bu.error(exception=err)
2018-10-08 00:14:56 +08:00
return None
def print_results(self):
print(' : '.join(self.get_results()))
2018-10-08 00:14:56 +08:00
2018-10-08 00:14:56 +08:00
class BattleRecordCategory():
def __init__(self, cat_name : str):
self.category_name = cat_name
self.category = collections.defaultdict(def_value_BattleRecord)
2018-12-14 23:33:38 +02:00
if result_categories[self.category_name][1] == 'string':
self.type = 'string'
elif result_categories[self.category_name][1] == 'number':
self.type = 'number'
elif result_categories[self.category_name][1] == 'total':
self.type = 'total'
else:
self.type = 'category'
2018-10-08 00:14:56 +08:00
def get_sub_categories(self):
2018-10-08 00:14:56 +08:00
return self.category.keys()
def record_result(self, result: dict):
2018-10-08 00:14:56 +08:00
try:
2018-12-14 23:33:38 +02:00
if self.type == 'total':
2018-10-08 00:14:56 +08:00
cat = 'Total'
2018-12-14 23:33:38 +02:00
elif self.type == 'number':
2018-10-08 00:14:56 +08:00
cat = str(result[self.category_name])
2018-12-14 23:33:38 +02:00
elif self.type == 'string':
cat = result[self.category_name]
2018-10-08 00:14:56 +08:00
else:
cat = result_categories[self.category_name][1][result[self.category_name]]
self.category[cat].record_result(result)
2018-10-08 00:14:56 +08:00
return True
except KeyError as err:
bu.error('Key not found', err)
2018-10-08 00:14:56 +08:00
except Exception as err:
bu.error(str(err))
return False
def get_headers(self):
2018-10-08 00:14:56 +08:00
try:
headers = [ RESULT_CAT_HEADER_FRMT.format(result_categories[self.category_name][0]) ]
2018-10-08 00:14:56 +08:00
for field in result_fields.keys():
print_format = '{:^' + str(result_fields[field][2]) + 's}'
headers.append(print_format.format(result_fields[field][0]))
return headers
except KeyError as err:
bu.error('Key not found', err)
2018-10-08 00:14:56 +08:00
except Exception as err:
bu.error(str(err))
return None
def print_results(self):
2018-10-08 00:14:56 +08:00
try:
print(' '.join(self.get_headers()))
for row in self.get_results():
2018-10-08 00:14:56 +08:00
print(' : '.join(row))
except KeyError as err:
bu.error('Key not found', err)
2018-10-08 00:14:56 +08:00
except Exception as err:
bu.error(str(err))
return None
def get_results(self):
2018-10-08 00:14:56 +08:00
try:
results = []
# results.append(self.get_headers())
2018-12-14 23:33:38 +02:00
if self.type == 'number':
for cat in sorted( [ int(s) for s in self.category.keys() ] ):
cat = str(cat)
row = [ RESULT_CAT_FRMT.format(cat) ]
row.extend(self.category[cat].get_results())
2018-12-14 23:33:38 +02:00
results.append(row)
else:
for cat in sorted(self.category.keys() , key=str.casefold):
row = [ RESULT_CAT_FRMT.format(cat) ]
row.extend(self.category[cat].get_results())
2018-12-14 23:33:38 +02:00
results.append(row)
2018-10-08 00:14:56 +08:00
return results
except KeyError as err:
bu.error('Key not found', err)
2018-10-08 00:14:56 +08:00
return None
## main() -------------------------------------------------------------
async def main(argv):
2018-11-05 14:48:07 +02:00
global wg, wi
# set the directory for the script
os.chdir(os.path.dirname(sys.argv[0]))
## Read config
config = configparser.ConfigParser()
config.read(FILE_CONFIG)
2020-02-08 12:07:26 +02:00
configGeneral = config['GENERAL']
# WG account id of the uploader:
# # Find it here: https://developers.wargaming.net/reference/all/wotb/account/list/
WG_ID = configGeneral.getint('wg_id', None)
USE_DB = configGeneral.getboolean('use_DB', False)
configDB = config['DATABASE']
DB_SERVER = configDB.get('db_server', 'localhost')
DB_PORT = configDB.getint('db_port', 27017)
DB_SSL = configDB.getboolean('db_ssl', False)
DB_CERT_REQ = configDB.getint('db_ssl_req', ssl.CERT_NONE)
DB_AUTH = configDB.get('db_auth', 'admin')
DB_NAME = configDB.get('db_name', 'BlitzStats')
DB_USER = configDB.get('db_user', 'mongouser')
DB_PASSWD = configDB.get('db_password', "PASSWORD")
DB_CERT = configDB.get('db_ssl_cert_file', None)
DB_CA = configDB.get('db_ssl_ca_file', None)
2018-10-08 00:14:56 +08:00
TASK_N = 7
2018-11-05 14:48:07 +02:00
parser = argparse.ArgumentParser(description='Analyze Blitz replay JSONs from WoTinspector.com')
2018-10-08 00:14:56 +08:00
parser.add_argument('--output', default='plain', choices=['json', 'plain', 'db'], help='Select output mode: JSON, plain text or database')
2020-02-08 12:07:26 +02:00
parser.add_argument('-id', dest='account_id', type=int, default=WG_ID, help='WG account_id to analyze')
2019-12-25 02:55:47 +02:00
parser.add_argument('-a', '--account', type=str, default=None, help='WG account nameto analyze. Format: ACCOUNT_NAME@SERVER')
2019-08-17 00:50:48 +03:00
parser.add_argument('-x', '--extended', action='store_true', default=False, help='Print Extended stats')
2019-12-24 12:30:09 +02:00
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')
2019-12-25 02:55:47 +02:00
parser.add_argument('-u', '--url', action='store_true', default=False, help='Print replay URLs')
2018-10-08 00:20:48 +08:00
parser.add_argument('--tankfile', type=str, default='tanks.json', help='JSON file to read Tankopedia from. Default is "tanks.json"')
parser.add_argument('--mapfile', type=str, default='maps.json', help='JSON file to read Blitz map names from. Default is "maps.json"')
2018-10-08 00:14:56 +08:00
parser.add_argument('-o','--outfile', type=str, default='-', metavar="OUTPUT", help='File to write results. Default STDOUT')
2020-02-08 12:07:26 +02:00
parser.add_argument('--db', action='store_true', default=USE_DB, help='Use DB - You are unlikely to have it')
2018-10-08 00:14:56 +08:00
parser.add_argument('-d', '--debug', action='store_true', default=False, help='Debug mode')
parser.add_argument('-v', '--verbose', action='store_true', default=False, help='Verbose mode')
2019-12-24 12:30:09 +02:00
parser.add_argument('-s', '--silent', action='store_true', default=False, help='Silent mode')
2018-11-05 14:48:07 +02:00
parser.add_argument('files', metavar='FILE1 [FILE2 ...]', type=str, nargs='+', help='Files to read. Use \'-\' for STDIN"')
2018-10-08 00:14:56 +08:00
args = parser.parse_args()
2019-12-24 12:30:09 +02:00
bu.set_log_level(args.silent, args.verbose, args.debug)
bu.set_progress_step(30) # Set the frequency of the progress dots.
wg = await WG.create(WG_APP_ID, args.tankfile, args.mapfile, True)
2018-11-05 14:48:07 +02:00
wi = WoTinspector()
2019-12-24 12:30:09 +02:00
2018-10-24 12:31:37 +03:00
if args.account != None:
2019-12-25 02:55:47 +02:00
args.account_id = await wg.get_account_id(args.account)
bu.debug('WG account_id: ' + str(args.account_id))
2018-10-08 00:14:56 +08:00
#### Connect to MongoDB (TBD)
2019-12-24 12:30:09 +02:00
bu.debug('DB_SERVER: ' + DB_SERVER)
bu.debug('DB_PORT: ' + str(DB_PORT))
bu.debug('DB_SSL: ' + "True" if DB_SSL else "False")
bu.debug('DB_AUTH: ' + DB_AUTH)
bu.debug('DB_NAME: ' + DB_NAME)
client = None
db = None
if args.db:
try:
client = motor.motor_asyncio.AsyncIOMotorClient(DB_SERVER,DB_PORT, authSource=DB_AUTH, username=DB_USER, password=DB_PASSWD, ssl=DB_SSL, ssl_cert_reqs=DB_CERT_REQ, ssl_certfile=DB_CERT, tlsCAFile=DB_CA)
db = client[DB_NAME]
bu.debug('Database connection initiated')
except Exception as err:
bu.error("Could no initiate DB connection: Disabling DB", err)
args.db = False
pass
else:
bu.debug('No DB in use')
2018-10-08 00:14:56 +08:00
try:
replayQ = asyncio.Queue()
2018-10-08 00:14:56 +08:00
reader_tasks = []
# Make replay Queue
scanner_task = asyncio.create_task(mk_replayQ(replayQ, args.files))
2019-08-17 00:50:48 +03:00
bu.debug('Replay scanner started')
2018-10-08 00:14:56 +08:00
# Start tasks to process the Queue
for i in range(TASK_N):
reader_tasks.append(asyncio.create_task(replay_reader(replayQ, i, args)))
2019-08-17 00:50:48 +03:00
bu.debug('ReplayReader ' + str(i) + ' started')
2018-10-08 00:14:56 +08:00
bu.debug('Waiting for the replay scanner to finish')
await asyncio.wait([scanner_task])
2019-08-17 00:50:48 +03:00
2018-10-08 00:14:56 +08:00
bu.debug('Scanner finished. Waiting for replay readers to finish the queue')
await replayQ.join()
2019-08-17 00:50:48 +03:00
await asyncio.sleep(0.1)
2018-10-08 00:14:56 +08:00
bu.debug('Replays read. Cancelling Readers and analyzing results')
for task in reader_tasks:
2019-08-17 00:50:48 +03:00
task.cancel()
await asyncio.sleep(0.1)
2018-10-08 00:14:56 +08:00
results = []
players = set()
2018-10-08 00:14:56 +08:00
for res in await asyncio.gather(*reader_tasks):
results.extend(res[0])
players.update(res[1])
2018-10-08 00:14:56 +08:00
# ## Remove battle_time from the player:tank data in case DB is not in use
# if db == None:
# players_tmp = set()
# for player in players:
# #bu.debug('playerID: ' + player + ' ==> ' + prune_stat_id(player))
# players_tmp.add(prune_stat_id(player))
# #bu.debug(str(len(players_tmp)))
# players = players_tmp
(player_stats, stat_id_map) = await process_player_stats(players, TASK_N, args, db)
2018-10-08 00:14:56 +08:00
bu.verbose('')
bu.debug('Number of player stats: ' + str(len(player_stats)))
teamresults = calc_team_stats(results, player_stats, stat_id_map, args)
process_battle_results(teamresults, args)
2020-02-08 12:07:26 +02:00
if args.hist:
print('\nPlayer Historgrams______', end='', flush=True)
process_player_dist(results, player_stats, stat_id_map)
bu.debug('Finished. Cleaning up..................')
2019-08-17 00:50:48 +03:00
except Exception as err:
bu.error(exception=err)
2018-10-08 00:14:56 +08:00
finally:
## Need to close the aiohttp.session since Python destructors do not support async methods...
2018-11-05 14:48:07 +02:00
await wg.close()
await wi.close()
2018-10-08 00:14:56 +08:00
return None
def process_player_dist(results: list, player_stats: dict, stat_id_map: dict):
"""Process player distribution"""
hist_stats = dict()
try:
for field in histogram_fields:
hist_stats[field] = PlayerHistogram(field, histogram_fields[field][0], histogram_fields[field][1], histogram_fields[field][2], histogram_fields[field][3] )
for result in results:
for player in result['allies'] | result['enemies']: # uniton of Sets
player_remapped = stat_id_map[player]
if player_remapped in player_stats:
if player in result['allies']:
for stat_field in hist_stats:
hist_stats[stat_field].record_ally(player_stats[player_remapped][stat_field])
else:
for stat_field in hist_stats:
hist_stats[stat_field].record_enemy(player_stats[player_remapped][stat_field])
for stat_field in hist_stats:
hist_stats[stat_field].print()
except Exception as err:
bu.error(exception=err)
return None
def process_battle_results(results: dict, args : argparse.Namespace):
"""Process replay battle results"""
url = args.url
urls = collections.OrderedDict()
2018-10-08 00:14:56 +08:00
categories = {}
2019-08-17 00:50:48 +03:00
cats = result_categories_default
if args.extended:
cats = result_categories.keys()
for cat in cats:
2018-10-08 00:14:56 +08:00
categories[cat] = BattleRecordCategory(cat)
max_title_len = 0
2018-10-08 00:14:56 +08:00
for result in results:
2019-08-17 00:50:48 +03:00
for cat in cats:
categories[cat].record_result(result)
if url:
max_title_len = max(max_title_len, len(result['title']))
urls[result['title']] = result['url']
2018-10-08 00:14:56 +08:00
2019-08-17 00:50:48 +03:00
for cat in cats:
for sub_cat in categories[cat].get_sub_categories():
categories[cat].category[sub_cat].calc_results()
2018-10-08 00:14:56 +08:00
print('')
categories[cat].print_results()
if url:
print('\nURLs to Battle replays:\n')
for title in urls.keys():
print(('{:' + str(3 + max_title_len) + '}').format(title) + ' : ' + urls[title])
2018-10-08 00:14:56 +08:00
return None
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"""
2018-10-08 00:14:56 +08:00
## 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()
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])
# create statsQ
for stat_id in stat_ids:
await statsQ.put(stat_id)
2018-10-08 00:14:56 +08:00
# Process player WR / battle stats
stats_tasks = []
bu.debug('Starting player stats workers')
2018-10-08 00:14:56 +08:00
for i in range(N_workers):
2019-08-17 00:50:48 +03:00
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')
2018-10-08 00:14:56 +08:00
await statsQ.join()
2019-08-17 00:50:48 +03:00
bu.debug('Cancelling stats workers')
2018-10-08 00:14:56 +08:00
for task in stats_tasks:
task.cancel()
player_stats = {}
stat_id_remap = {}
2018-10-08 00:14:56 +08:00
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}
## 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)
def remap_stat_id(stat_id_map: dict, stat_id_remap: dict):
bu.debug('Remapping stat_ids: stat_id_map: ' + str(len(stat_id_map)) + ' remap needed: ' + str(len(stat_id_remap)))
for stat_id in stat_id_map:
# bu.debug(stat_id)
if stat_id_map[stat_id] in stat_id_remap:
stat_id_map[stat_id] = stat_id_remap[stat_id_map[stat_id]]
return stat_id_map
def calc_team_stats(results: list, player_stats : dict, stat_id_map : dict, args : argparse.Namespace) -> list:
"""Calculate team stats"""
2018-10-08 00:14:56 +08:00
return_list = []
2018-10-08 00:14:56 +08:00
stat_types = player_stats[list(player_stats.keys())[0]].keys()
bu.debug('stat_types: ' + ','.join(stat_types))
for result in results:
2018-10-08 00:14:56 +08:00
try:
n_allies = collections.defaultdict(def_value_zero)
allies_stats = collections.defaultdict(def_value_zero)
#bu.debug('Processing Allies')
2018-10-08 00:14:56 +08:00
for ally in result['allies']:
# Player itself is not in 'allies': see read_replay_JSON()
ally_mapped = stat_id_map[ally]
if ally_mapped not in player_stats:
continue
2018-10-08 00:14:56 +08:00
for stat in stat_types:
if player_stats[ally_mapped][stat] != None:
allies_stats[stat] += player_stats[ally_mapped][stat]
2018-10-08 21:24:46 +08:00
n_allies[stat] += 1
#bu.debug('Processing Allies\' avg stats')
2018-10-08 00:14:56 +08:00
for stat in stat_types:
if n_allies[stat] > 0:
result['allies_' + str(stat)] = allies_stats[stat] / n_allies[stat]
#bu.debug('Processing Enemies')
n_enemies = collections.defaultdict(def_value_zero)
enemies_stats = collections.defaultdict(def_value_zero)
2018-10-08 00:14:56 +08:00
for enemy in result['enemies']:
enemy_mapped = stat_id_map[enemy]
if enemy_mapped not in player_stats: continue
2018-10-08 00:14:56 +08:00
for stat in stat_types:
if player_stats[enemy_mapped][stat] != None:
enemies_stats[stat] += player_stats[enemy_mapped][stat]
2018-10-08 21:24:46 +08:00
n_enemies[stat] += 1
#bu.debug('Processing Enemies\' avg stats')
2018-10-08 00:14:56 +08:00
for stat in stat_types:
if n_enemies[stat] > 0:
result['enemies_' + str(stat)] = enemies_stats[stat] / n_enemies[stat]
2018-10-08 00:14:56 +08:00
return_list.append(result)
except KeyError as err:
bu.error('Key not found', err)
2018-10-08 00:14:56 +08:00
except Exception as err:
bu.error(exception=err)
2018-10-08 00:14:56 +08:00
return return_list
async def stat_worker(queue : asyncio.Queue, workerID: int, args : argparse.Namespace, db : motor.motor_asyncio.AsyncIOMotorDatabase) -> list:
2018-10-08 00:14:56 +08:00
"""Worker thread to find stats for player / tank pairs"""
# global wg
stats = {}
stat_id_remap = {}
stat_db_func = globals()[STAT_FUNC[args.stat_func][1]]
stat_wg_func = globals()[STAT_FUNC[args.stat_func][2]]
2019-08-17 00:50:48 +03:00
bu.debug("workedID: " + str(workerID) + ' started')
2018-10-08 00:14:56 +08:00
try:
while True:
stat_id = await queue.get()
2018-10-08 00:14:56 +08:00
try:
bu.debug('Stat_id: ' + stat_id)
bu.print_progress()
# Analysing player performance based on their stats on the tier tanks they are playing
stats[stat_id] = await stat_db_func(db, stat_id)
bu.debug('get_db_' + args.stat_func + '_stats returned: ' + str(stats[stat_id]), workerID)
if (stats[stat_id] == None):
pruned_stat_id = prune_stat_id(stat_id)
if (pruned_stat_id not in stats):
stats[pruned_stat_id] = await stat_wg_func(pruned_stat_id)
stat_id_remap[stat_id] = pruned_stat_id
del stats[stat_id]
2018-10-08 00:14:56 +08:00
except KeyError as err:
bu.error('Key not found', err, workerID)
2018-10-08 00:14:56 +08:00
except Exception as err:
bu.error('Unexpected error', err, workerID)
2018-10-08 00:14:56 +08:00
queue.task_done()
2019-08-17 00:50:48 +03:00
except (asyncio.CancelledError, concurrent.futures._base.CancelledError):
bu.debug('Stats queue is empty', workerID)
2018-10-08 00:14:56 +08:00
except Exception as err:
bu.error(exception=err)
finally:
# remove empty stats
try:
keys_2_del = []
for key in stats:
if stats[key] == None:
keys_2_del.append(key)
for key in keys_2_del:
del stats[key]
except KeyError as err:
bu.error('Error in pruning empty stats', err)
# bu.debug('Returning stats & exiting')
return (stats, stat_id_remap)
2018-10-08 00:14:56 +08:00
## player stat functions: tank_tier
async def get_wg_tank_tier_stats(stat_id_str: str) -> dict:
"""Get player stats from WG. Returns WR per tier of tank_id"""
try:
(account_id, tier) = str2ints(stat_id_str)
tier_tanks = wg.get_tanks_by_tier(tier)
# 'battles' must always be there
hist_stats = [ 'all.' + x for x in histogram_fields.keys() ]
hist_stats.append('tank_id')
2020-02-08 12:07:26 +02:00
player_stats = await wg.get_player_tank_stats(account_id, tier_tanks ,hist_stats)
#bu.debug('account_id: ' + str(account_id) + ' ' + str(player_stats))
return await tank_stats_helper(player_stats)
except KeyError as err:
bu.error('account_id: ' + str(account_id) + ' Key not found', err)
except Exception as err:
bu.error(exception=err)
return None
async def get_db_tank_tier_stats(db : motor.motor_asyncio.AsyncIOMotorDatabase, stat_id_str: str) -> dict:
"""Get player stats from MongoDB (you are very unlikely to have it, unless you have set it up)"""
if db == None:
return None
try:
dbc = db[DB_C_TANK_STATS]
( account_id, tier, battletime ) = str2ints(stat_id_str)
tier_tanks = wg.get_tanks_by_tier(tier)
time_buffer = 2*7*24*3600
#bu.debug('Tank_ids: ' + ','.join(map(str, tier_tanks)))
# if battletime == None:
# pipeline = [ { '$match': { '$and': [ { 'account_id': account_id }, { 'tank_id' : {'$in': tier_tanks} } ]}},
# { '$sort': { 'last_battle_time': -1 }},
# { '$group': { '_id': '$tank_id', 'doc': { '$first': '$$ROOT' }}},
# { '$replaceRoot': { 'newRoot': '$doc' }},
# { '$project': { '_id': 0 }} ]
# else:
pipeline = [ { '$match': { '$and': [ { 'account_id': account_id }, { 'last_battle_time': { '$lte': battletime + time_buffer }}, { 'tank_id' : {'$in': tier_tanks} } ]}},
{ '$sort': { 'last_battle_time': -1 }},
{ '$group': { '_id': '$tank_id', 'doc': { '$first': '$$ROOT' }}},
{ '$replaceRoot': { 'newRoot': '$doc' }},
{ '$project': { '_id': 0 }} ]
# cursor = dbc.aggregate(pipeline, allowDiskUse=True)
cursor = dbc.aggregate(pipeline)
return await tank_stats_helper(await cursor.to_list(1000))
except Exception as err:
bu.error('account_id: ' + str(account_id) + ' Error', err)
return None
## player stat functions: player
async def get_db_player_stats(db : motor.motor_asyncio.AsyncIOMotorDatabase, stat_id_str: str) -> dict:
"""Get player stats from MongoDB (you are very unlikely to have it, unless you have set it up)"""
if db == None:
return None
try:
dbc = db[DB_C_TANK_STATS] ## not collecting player stats yet 2019-12-23
( account_id, battletime ) = str2ints(stat_id_str)
time_buffer = 2*7*24*3600
pipeline = [ { '$match': { '$and': [ { 'account_id': account_id }, { 'last_battle_time': { '$lte': battletime + time_buffer }} ]}},
{ '$sort': { 'last_battle_time': -1 }},
{ '$group': { '_id': '$tank_id', 'doc': { '$first': '$$ROOT' }}},
{ '$replaceRoot': { 'newRoot': '$doc' }},
{ '$project': { '_id': 0 }} ]
# cursor = dbc.aggregate(pipeline, allowDiskUse=True)
cursor = dbc.aggregate(pipeline)
return await tank_stats_helper(await cursor.to_list(1000))
except Exception as err:
bu.error('account_id: ' + str(account_id) + ' Error', err)
return None
async def get_wg_player_stats(stat_id_str: str) -> dict:
"""Get player stats from WG. Returns WR per tier of tank_id"""
try:
account_id = int(stat_id_str)
# 'battles' must always be there
hist_stats = [ 'all.' + x for x in histogram_fields.keys() ]
hist_stats.append('tank_id')
player_stats = await wg.get_player_stats(account_id, hist_stats)
#bu.debug('account_id: ' + str(account_id) + ' ' + str(player_stats))
return await player_stats_helper(player_stats)
except KeyError as err:
bu.error('account_id: ' + str(account_id) + ' Key not found', err)
except Exception as err:
bu.error(exception=err)
return None
async def tank_stats_helper(stat_list: list):
"""Helpher func got get_db_tank_tier_stats() and get_wg_tank_tier_stats()"""
try:
if stat_list == None:
return None
stats = {}
hist_fields = histogram_fields.keys()
if 'battles' not in hist_fields:
bu.error('\'battles\' must be defined in \'histogram_fields\'')
sys.exit(1)
for field in hist_fields:
stats[field] = 0
for tank_stat in stat_list:
tank_stat = tank_stat['all']
for field in hist_fields:
stats[field] += max(0, tank_stat[field])
if stats['battles'] == 0:
return None
for field in hist_fields:
if field == 'battles': continue
stats[field] = stats[field] / stats['battles']
return stats
except Exception as err:
bu.error(exception=err)
return None
async def player_stats_helper(player_stats: dict):
"""Extract relevant stats for player histogram from raw player stats"""
try:
if player_stats == None:
return None
stats = {}
hist_fields = histogram_fields.keys()
if 'battles' not in hist_fields:
bu.error('\'battles\' must be defined in \'histogram_fields\'')
sys.exit(1)
for field in hist_fields:
stats[field] = 0
for field in hist_fields:
stats[field] += max(0, player_stats['statistics']['all'][field])
if stats['battles'] == 0:
return None
for field in hist_fields:
if field == 'battles': continue
stats[field] = stats[field] / stats['battles']
return stats
except Exception as err:
bu.error(exception=err)
return None
async def mk_replayQ(queue : asyncio.Queue, files : list):
2018-10-08 00:14:56 +08:00
"""Create queue of replays to post"""
p_replayfile = re.compile('.*\\.wotbreplay\\.json$')
if files[0] == '-':
bu.debug('reading replay file list from STDIN')
stdin, _ = await aioconsole.get_standard_streams()
while True:
line = (await stdin.readline()).decode('utf-8').rstrip()
if not line:
break
else:
if (p_replayfile.match(line) != None):
await queue.put(await mk_readerQ_item(line))
2018-10-08 00:14:56 +08:00
else:
for fn in files:
2019-08-17 00:50:48 +03:00
# bu.debug('Filename: ' + fn)
2018-11-05 18:15:51 +02:00
if fn.endswith('"'):
fn = fn[:-1]
2018-10-08 00:14:56 +08:00
if os.path.isfile(fn) and (p_replayfile.match(fn) != None):
await queue.put(await mk_readerQ_item(fn))
2019-08-17 00:50:48 +03:00
bu.debug('File added to queue: ' + fn)
2018-10-08 00:14:56 +08:00
elif os.path.isdir(fn):
with os.scandir(fn) as dirEntry:
for entry in dirEntry:
if entry.is_file() and (p_replayfile.match(entry.name) != None):
bu.debug(entry.name)
await queue.put(await mk_readerQ_item(entry.path))
2019-08-17 00:50:48 +03:00
bu.debug('File added to queue: ' + entry.path)
bu.debug('Finished: ' + str(queue.qsize()) + ' replays to process')
2018-10-08 00:14:56 +08:00
return None
async def mk_readerQ_item(filename : str) -> list:
2018-10-08 00:14:56 +08:00
"""Make an item to replay queue"""
global REPLAY_N
REPLAY_N +=1
return [filename, REPLAY_N]
async def replay_reader(queue: asyncio.Queue, readerID: int, args : argparse.Namespace):
2018-10-08 00:14:56 +08:00
"""Async Worker to process the replay queue"""
#global SKIPPED_N
2019-12-25 02:55:47 +02:00
# account_id = args.account_id
2018-10-08 00:14:56 +08:00
results = []
playerstanks = set()
try:
while True:
item = await queue.get()
filename = item[0]
replayID = item[1]
try:
replay_json = await bu.open_JSON(filename, wi.chk_JSON_replay)
2018-11-05 14:48:07 +02:00
if replay_json == None:
bu.verbose('Replay[' + str(replayID) + ']: ' + filename + ' is invalid. Skipping.' )
#SKIPPED_N += 1
queue.task_done()
continue
2018-10-08 00:14:56 +08:00
2018-11-05 14:48:07 +02:00
## Read the replay JSON
bu.debug('reading replay: ' + filename, readerID)
result = await read_replay_JSON(replay_json, args)
bu.print_progress()
2018-11-05 14:48:07 +02:00
if result == None:
bu.error('Replay[' + str(replayID) + ']: Error reading: ' + filename, id=readerID)
2018-11-05 14:48:07 +02:00
queue.task_done()
continue
# if (account_id != None):
playerstanks.update(set(result['allies']))
playerstanks.update(set(result['enemies']))
2018-11-05 14:48:07 +02:00
results.append(result)
bu.debug('Marking task ' + str(replayID) + ' done')
2018-10-08 00:14:56 +08:00
except Exception as err:
bu.error(exception=err)
2018-10-08 00:14:56 +08:00
queue.task_done()
2019-08-17 00:50:48 +03:00
except (asyncio.CancelledError, concurrent.futures.CancelledError):
bu.debug( str(len(results)) + ' replays, ' + str(len(playerstanks)) + ' player/tanks', readerID)
2018-10-08 00:14:56 +08:00
return results, playerstanks
return None
async def read_replay_JSON(replay_json: dict, args : argparse.Namespace) -> dict:
2018-10-08 00:14:56 +08:00
"""Parse replay JSON dict"""
2019-08-17 00:50:48 +03:00
global REPLAY_I
2019-12-25 02:55:47 +02:00
account_id = args.account_id
2018-10-08 00:14:56 +08:00
url = args.url
#db = args.db
2018-10-08 00:14:56 +08:00
result = {}
try:
if account_id == None:
account_id = replay_json['data']['summary']['protagonist']
elif replay_json['data']['summary']['protagonist'] != account_id:
if account_id in replay_json['data']['summary']['enemies']:
# switch the teams...
if replay_json['data']['summary']['battle_result'] != 2:
if replay_json['data']['summary']['battle_result'] == 0:
replay_json['data']['summary']['battle_result'] = 1
else:
replay_json['data']['summary']['battle_result'] = 0
tmp = replay_json['data']['summary']['enemies']
replay_json['data']['summary']['enemies'] = replay_json['data']['summary']['allies']
replay_json['data']['summary']['allies'] = tmp
elif account_id not in replay_json['data']['summary']['allies']:
# account_id looked for but not found in teams
bu.debug('Replay ' + replay_json['data']['summary']['title'] + ' does not have account_id ' + str(account_id) + '. Skipping.')
return None
if url:
result['url'] = replay_json['data']['view_url']
for key in replay_summary_flds:
result[key] = replay_json['data']['summary'][key]
2018-11-05 14:48:07 +02:00
result['battle_start_timestamp'] = int(replay_json['data']['summary']['battle_start_timestamp'])
2018-10-08 00:14:56 +08:00
result['allies'] = set()
result['enemies'] = set()
btl_duration = 0
btl_tier = 0
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')
2018-10-08 00:14:56 +08:00
btl_tier = max(btl_tier, player_tank_tier)
2018-10-08 00:14:56 +08:00
if player['dbid'] == account_id:
# player itself is not put in results['allies']
2018-10-08 00:14:56 +08:00
tmp = {}
tmp['account_id'] = account_id
tmp['tank_id'] = player['vehicle_descr']
tmp['tank_tier'] = player_tank_tier
tmp['tank_name'] = wg.get_tank_data(tmp['tank_id'], 'name')
tmp['squad_index'] = player['squad_index']
2018-10-08 00:14:56 +08:00
for key in replay_details_flds:
tmp[key] = player[key]
if player['hitpoints_left'] == 0:
tmp['survived'] = 0
tmp['destroyed'] = 1
else:
tmp['survived'] = 1
tmp['destroyed'] = 0
for key in tmp.keys():
result[key] = tmp[key]
else:
tmp_account_id = player['dbid']
tmp_tank_id = player['vehicle_descr']
# tmp_tank_tier = wg.get_tank_tier(tmp_tank_id)
tmp_battletime = int(replay_json['data']['summary']['battle_start_timestamp'])
if player['dbid'] in replay_json['data']['summary']['allies']:
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))
# remove platoon buddy from stats
if result['squad_index'] != None:
for player in replay_json['data']['summary']['details']:
bu.debug(str(player))
if (player['squad_index'] == result['squad_index']) and (player['dbid'] in replay_json['data']['summary']['allies']) and (player['dbid'] != account_id):
# platoon buddy found
tmp_account_id = player['dbid']
tmp_tank_id = player['vehicle_descr']
#tmp_tank_tier = str(wg.get_tank_tier(tmp_tank_id))
tmp_battletime = int(replay_json['data']['summary']['battle_start_timestamp'])
# platoon buddy removed from stats
result['allies'].remove(get_stat_id(tmp_account_id, tmp_tank_id, tmp_battletime))
2018-10-08 00:14:56 +08:00
result['time_alive%'] = result['time_alive'] / btl_duration
result['battle_tier'] = btl_tier
result['top_tier'] = 1 if (result['tank_tier'] == btl_tier) else 0
result['win'] = 1 if result['battle_result'] == 1 else 0
2019-08-17 00:50:48 +03:00
REPLAY_I += 1
result['battle_i'] = REPLAY_I
2018-10-08 00:14:56 +08:00
bu.debug(str(result))
return result
except KeyError as err:
bu.error('Key not found', err)
2018-10-08 00:14:56 +08:00
except Exception as err:
bu.error(exception=err)
2018-10-08 00:14:56 +08:00
return None
def str2ints(stat_id_str: str) -> list:
"""Convert stat_id string e.g. 'account_id:tank_id' to list of ints"""
return [ int(x) for x in stat_id_str.split(':') ]
def prune_stat_id(stat_id_str: str) -> str:
stat_id = stat_id_str.split(':')
del stat_id[-1]
return ':'.join(stat_id)
def get_stat_id(account_id: int, tank_id: int, battletime: int) -> str:
return ':'.join([ str(account_id), str(tank_id), str(battletime)])
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]))
return ':'.join([ stat_id[0], str(tank_tier), stat_id[2] ])
def get_stat_id_player(stat_id_str: str) -> str:
"""Return stat_id = account_id:tank_tier"""
stat_id = stat_id_str.split(':')
return ':'.join([ stat_id[0], stat_id[2] ])
2018-10-08 00:14:56 +08:00
### main()
if __name__ == "__main__":
2018-12-15 02:47:21 +02:00
asyncio.run(main(sys.argv[1:]), debug=False)