Author SHA1 Message Date
Jylpah 4c517a11c5 Update blitzutils.py 2020-12-06 16:33:20 +02:00
Jylpah 1a7fcffa17 WIReplay(): started 2020-12-05 23:19:05 +02:00
8 changed files with 2432 additions and 4475 deletions

No files matched your search

+99 -434
View File
@@ -1,10 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3.8
## PYTHON VERSION MUST BE 3.7 OR HIGHER
# Script Analyze WoT Blitz replays # Script Analyze WoT Blitz replays
import sys, argparse, json, os, concurrent, inspect, aiohttp, asyncio, aiofiles, aioconsole import sys, argparse, json, os, concurrent, inspect, aiohttp, asyncio, aiofiles, aioconsole
import motor.motor_asyncio, configparser, ssl, re, logging, time, xmltodict, collections, csv import motor.motor_asyncio, configparser, ssl, re, logging, time, xmltodict, collections
from datetime import datetime, timedelta, date from datetime import datetime, timedelta, date
import blitzutils as bu import blitzutils as bu
from blitzutils import WG from blitzutils import WG
@@ -30,64 +29,13 @@ BATTLE_TIME_BUCKET = 3600*24*14
RE_SRC_IS_DB = re.compile(r'^DB:') RE_SRC_IS_DB = re.compile(r'^DB:')
class StatFunc:
## For different player stat functions (global stats, tank tier stats, etc) ## 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 # 1st function = for forming stat_id, 2nd for DB stats query, 3rd for WG API stats query
STAT_FUNC = { STAT_FUNC = {
'tank_tier': [ 'get_stat_id_tank_tier', 'get_db_tank_tier_stats', 'get_wg_tank_tier_stats', 'WR at tier' ], 'tank_tier': [ 'get_stat_id_tank_tier', 'get_db_tank_tier_stats', 'get_wg_tank_tier_stats' ],
'tank': [ 'get_stat_id_tank', 'get_db_tank_stats', 'get_wg_tank_stats', 'WR in Tank' ], 'tank': [ 'get_stat_id_tank', 'get_db_tank_stats', 'get_wg_tank_stats' ],
'player': [ 'get_stat_id_player', 'get_db_player_stats', 'get_wg_player_stats', 'Career WR' ], 'player': [ 'get_stat_id_player', 'get_db_player_stats', 'get_wg_player_stats' ]
'tier_x': [ 'get_stat_id_player', 'get_db_tier_x_stats', 'get_wg_tier_x_stats', 'WR at tier X' ], }
}
_DEFAULT = 'player'
_stat_func = _DEFAULT
@classmethod
def get_default(cls):
return cls._DEFAULT
@classmethod
def get_stat_funcs(cls):
return cls.STAT_FUNC.keys()
@classmethod
def set_stat_func(cls, stat_func: str = _DEFAULT):
if stat_func in cls.STAT_FUNC.keys():
cls._stat_func = stat_func
else:
## ERROR
pass
return cls._stat_func
@classmethod
def get_stat_func(cls):
return cls._stat_func
@classmethod
def get_stat_id_func(cls):
return cls.STAT_FUNC[cls._stat_func][0]
@classmethod
def get_db_func(cls):
return cls.STAT_FUNC[cls._stat_func][1]
@classmethod
def get_wg_func(cls):
return cls.STAT_FUNC[cls._stat_func][2]
@classmethod
def get_title(cls):
return cls.STAT_FUNC[cls._stat_func][3]
replay_summary_flds = [ replay_summary_flds = [
'battle_result', 'battle_result',
@@ -131,7 +79,7 @@ replay_details_flds = [
## Syntax: key == stat field in https://api.wotblitz.eu/wotb/tanks/stats/ (all.KEY) ## 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' ] ## Value array [ 'Stat Title', [ 0, data buckets ....], scaling_factor_for_bucket_values, 'print_format' ]
histogram_fields = { histogram_fields = {
'wins' : [ 'Win rate', [0, .35, .40, .45, .48, .5, .52, .55, .60, .65, .70, 1], 1, '{:2.0%}' ], 'wins' : [ 'Win rate', [0, .35, .40, .45, .48, .5, .52, .55, .60, .65, .70, 1], 100, '{:2.0f}%' ],
'damage_dealt' : [ 'Avg. Dmg.', [0, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500, 1750, 2000, 2250, 2500, 2750, 3000, 100e3], 1, '{:3.0f}' ], 'damage_dealt' : [ 'Avg. Dmg.', [0, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500, 1750, 2000, 2250, 2500, 2750, 3000, 100e3], 1, '{:3.0f}' ],
'battles' : [ 'Battles', [0, 1000, 2500, 5000, 7000, 10e3, 15e3, 25e3, 50e3, 5e7], .001, '{:.0f}k'] # battles is a mandatory stat to include 'battles' : [ 'Battles', [0, 1000, 2500, 5000, 7000, 10e3, 15e3, 25e3, 50e3, 5e7], .001, '{:.0f}k'] # battles is a mandatory stat to include
} }
@@ -162,7 +110,7 @@ class BattleCategorizationList():
_battle_modes = { _battle_modes = {
'Any' : 0, 'Any' : 0,
'Regular' : 1, 'Random' : 1,
'Training Room' : 2, 'Training Room' : 2,
'Tournament' : 4, 'Tournament' : 4,
'Rating' : 7, 'Rating' : 7,
@@ -196,17 +144,10 @@ class BattleCategorizationList():
'enemies_spotted' : [ 'Player Spots', 'number'], 'enemies_spotted' : [ 'Player Spots', 'number'],
'hit_rate' : [ 'Player Hit Rate', 'bucket', [ 0, .25, .5, .6, .7, .8, .9, .95 ], '%' ], 'hit_rate' : [ 'Player Hit Rate', 'bucket', [ 0, .25, .5, .6, .7, .8, .9, .95 ], '%' ],
'pen_rate' : [ 'Player Pen Rate', 'bucket', [ 0, .25, .5, .6, .7, .8, .9, .95 ], '%' ], 'pen_rate' : [ 'Player Pen Rate', 'bucket', [ 0, .25, .5, .6, .7, .8, .9, .95 ], '%' ],
'alive' : [ 'Time alive (pcs)', 'bucket', [ x/100 for x in range(0,100,10) ], '%' ], 'player_name' : [ 'Player', 'string' ],
'time_alive' : [ 'Time alive (s)', 'bucket', [ x*60 for x in range(0,8) ], 'int' ],
'battle_duration' : [ 'Battle Duration', 'bucket', [ x*60 for x in range(0,8) ], 'int' ],
'distance_travelled' : [ 'Distance Driven', 'bucket', [ x*250 for x in range(0,20) ], 'int' ],
'allies_wins' : [ 'Allies WR', 'bucket', [ 0, .35, .45, .50, .55, .65], '%' ],
'enemies_wins' : [ 'Enemies WR', 'bucket', [ 0, .35, .45, .50, .55, .65], '%' ],
'player_name' : [ 'Player', 'string', 25 ],
'protagonist' : [ 'account_id', 'number' ], 'protagonist' : [ 'account_id', 'number' ],
'tank_name' : [ 'Tank', 'string', 25], 'tank_name' : [ 'Tank', 'string' ],
'map_name' : [ 'Map', 'string', 20], 'map_name' : [ 'Map', 'string' ],
'battle' : [ 'Battle', 'string', 40],
'battle_i' : [ 'Battle #', 'number' ] 'battle_i' : [ 'Battle #', 'number' ]
} }
@@ -288,7 +229,7 @@ class BattleCategorizationList():
if ctype == 'number': if ctype == 'number':
cat_tmp = BattleNumberCategorization(cat, ctitle) cat_tmp = BattleNumberCategorization(cat, ctitle)
elif ctype == 'string': elif ctype == 'string':
cat_tmp = BattleStringCategorization(cat, ctitle, cparams) cat_tmp = BattleStringCategorization(cat, ctitle)
elif ctype == 'category': elif ctype == 'category':
cat_tmp = BattleClassCategorization(cat, ctitle, cparams[0]) cat_tmp = BattleClassCategorization(cat, ctitle, cparams[0])
elif ctype == 'bucket': elif ctype == 'bucket':
@@ -340,7 +281,7 @@ class BattleCategorizationList():
elif ctype == 'number': elif ctype == 'number':
self.categorizations[cat] = BattleNumberCategorization(cat, ctitle) self.categorizations[cat] = BattleNumberCategorization(cat, ctitle)
elif ctype == 'string': elif ctype == 'string':
self.categorizations[cat] = BattleStringCategorization(cat, ctitle, cparams) self.categorizations[cat] = BattleStringCategorization(cat, ctitle)
elif ctype == 'category': elif ctype == 'category':
self.categorizations[cat] = BattleClassCategorization(cat, ctitle, cparams[0]) self.categorizations[cat] = BattleClassCategorization(cat, ctitle, cparams[0])
elif ctype == 'bucket': elif ctype == 'bucket':
@@ -438,28 +379,9 @@ class BattleCategorizationList():
bu.error(exception=err) bu.error(exception=err)
return None return None
def get_results_list(self, urls: bool = False):
try:
res = list()
for cat in self.categorizations_list:
res.append('')
res.extend(self.categorizations[cat].get_results_list())
if urls:
res.append('')
for url in self.urls:
res.append(url)
return res
except Exception as err:
bu.error(exception=err)
return None
class BattleCategorization(): class BattleCategorization():
RESULT_CAT_HEADER_LEN = 15 RESULT_CAT_FRMT = '{:>20s}'
RESULT_CAT_HEADER_FRMT = '{:_<' + str(RESULT_CAT_HEADER_LEN) + 's}'
RESULT_CAT_FRMT = '{:>' + str(RESULT_CAT_HEADER_LEN) + 's}'
def __init__(self, cat_key : str, title: str): def __init__(self, cat_key : str, title: str):
self.total_battles = 0 self.total_battles = 0
@@ -468,15 +390,6 @@ class BattleCategorization():
self.categories = collections.defaultdict(def_value_BattleCategory) self.categories = collections.defaultdict(def_value_BattleCategory)
def set_cat_header_format(self, header_len: int, left_aligned: bool = False):
self.RESULT_CAT_HEADER_LEN = header_len
self.RESULT_CAT_HEADER_FRMT = '{:_<' + str(header_len) + 's}'
if left_aligned:
self.RESULT_CAT_FRMT = '{:<' + str(header_len) + 's}'
else:
self.RESULT_CAT_FRMT = '{:>' + str(header_len) + 's}'
def get_categories(self): def get_categories(self):
"""Get category keys sorted""" """Get category keys sorted"""
return sorted(self.categories.keys() , key=str.casefold) return sorted(self.categories.keys() , key=str.casefold)
@@ -500,7 +413,7 @@ class BattleCategorization():
print(FORMAT.format(self.category_key, self.title), end=' ') print(FORMAT.format(self.category_key, self.title), end=' ')
def record_result(self, result: dict) -> bool: def record_result(self, result: dict):
try: try:
cat = self.get_category(result) cat = self.get_category(result)
self.categories[cat].record_result(result) self.categories[cat].record_result(result)
@@ -521,38 +434,12 @@ class BattleCategorization():
bu.error(exception = err) bu.error(exception = err)
def print_headers(self):
try:
if len(self.title) > self.RESULT_CAT_HEADER_LEN:
cat_name = self.title[:self.RESULT_CAT_HEADER_LEN]
else:
cat_name = self.title
header = self.RESULT_CAT_HEADER_FRMT.format(cat_name)
BattleCategory.print_headers(header)
return None
except Exception as err:
bu.error(exception = err)
def get_headers(self) -> list:
"""Get BattleCategory headers as list"""
try:
return BattleCategory.get_headers(self.title)
except Exception as err:
bu.error(exception = err)
return None
def print_results(self): def print_results(self):
try: try:
bu.print_new_line() bu.print_new_line()
self.print_headers() BattleCategory.print_headers(self.title)
for cat in self.get_categories(): for cat in self.get_categories():
if len(cat) > self.RESULT_CAT_HEADER_LEN: print(self.RESULT_CAT_FRMT.format(cat), end=' ')
cat_name = cat[:self.RESULT_CAT_HEADER_LEN]
else:
cat_name = cat
print(self.RESULT_CAT_FRMT.format(cat_name), end=' ')
self.categories[cat].print_results() self.categories[cat].print_results()
except KeyError as err: except KeyError as err:
bu.error('Key not found', err) bu.error('Key not found', err)
@@ -561,7 +448,7 @@ class BattleCategorization():
return None return None
def get_results_json(self) -> dict: def get_results_json(self):
"""Get results in JSON format""" """Get results in JSON format"""
try: try:
results = dict() results = dict()
@@ -576,19 +463,6 @@ class BattleCategorization():
return None return None
def get_results_list(self) -> list:
"""Get results in list format (for CSV printing)"""
try:
results = list()
results.append(self.get_headers())
for cat in self.get_categories():
results.append( [cat] + self.categories[cat].get_results_list() )
return results
except KeyError as err:
bu.error('Key not found', err)
return None
class BattleTotals(BattleCategorization): class BattleTotals(BattleCategorization):
TOTAL = 'Total' TOTAL = 'Total'
@@ -672,17 +546,6 @@ class BattleClassCategorization(BattleCategorization):
class BattleStringCategorization(BattleCategorization): class BattleStringCategorization(BattleCategorization):
"""Class for categorization by string""" """Class for categorization by string"""
def __init__(self, cat_key : str, title: str, params: list):
super().__init__(cat_key, title)
if params != None and len(params) > 0:
width = params[0]
left_align = True # align long category names left by default
if len(params) > 1:
left_align = params[1]
self.set_cat_header_format(width, left_align)
def get_category(self, result: dict) -> str: def get_category(self, result: dict) -> str:
"""Get category""" """Get category"""
try: try:
@@ -694,21 +557,6 @@ class BattleStringCategorization(BattleCategorization):
return None return None
def get_categories(self):
"""Get category keys sorted"""
try:
p_number_sort = re.compile(r'^(\d+): .+')
cats = list(self.categories.keys())
if (p_number_sort.match(cats[0]) != None):
ndxs = sorted(range(len(cats)), key=lambda k: int(cats[k][:cats[k].find(':')]) )
return [ cats[i] for i in ndxs ]
else:
return sorted(cats , key=str.casefold)
except Exception as err:
bu.error('Category: ' + self.category_key, exception=err)
return None
def get_filter_categories(self, filters: list) -> list: def get_filter_categories(self, filters: list) -> list:
try: try:
return filters return filters
@@ -899,10 +747,7 @@ class BattleCategory():
'hit_rate' : [ 'Hit rate', 'Shots hit / all shots made', 8, '{:8.1%}' ], 'hit_rate' : [ 'Hit rate', 'Shots hit / all shots made', 8, '{:8.1%}' ],
'pen_rate' : [ 'Pen rate', 'Shots pen / shots hit', 8, '{:8.1%}' ], 'pen_rate' : [ 'Pen rate', 'Shots pen / shots hit', 8, '{:8.1%}' ],
'survived' : [ 'Surv%', 'Survival rate', 6, '{:6.1%}' ], 'survived' : [ 'Surv%', 'Survival rate', 6, '{:6.1%}' ],
'time_alive' : [ 'T alive', 'Time being alive in a battle in secs', 7, '{:7.0f}' ], 'time_alive%' : [ 'T alive%', 'Percentage of time being alive in a battle', 8, '{:8.0%}' ],
'alive' : [ 'Share live', 'Percentage of time being alive in a battle', 8, '{:8.0%}' ],
'battle_duration' : [ 'Duration', 'Battle duration', 8, '{:8.0f}' ],
'distance_travelled': [ 'Distance', 'Distance travelled', 8, '{:8.0f}' ],
'top_tier' : [ 'Top tier', 'Share of games as top tier', 8, '{:8.0%}' ], 'top_tier' : [ 'Top tier', 'Share of games as top tier', 8, '{:8.0%}' ],
'player_wins' : [ 'Player WR', 'Average WR of the player', 9, '{:9.2%}' ], 'player_wins' : [ 'Player WR', 'Average WR of the player', 9, '{:9.2%}' ],
'allies_wins' : [ 'Allies WR', 'Average WR of allies at the tier of their tank', 9, '{:9.2%}' ], 'allies_wins' : [ 'Allies WR', 'Average WR of allies at the tier of their tank', 9, '{:9.2%}' ],
@@ -919,16 +764,10 @@ class BattleCategory():
# fields to display in results # fields to display in results
_result_fields_modes = { _result_fields_modes = {
'default' : [ 'battles', 'win','damage_made', 'enemies_destroyed', 'enemies_spotted', 'default' : [ 'battles', 'win','damage_made', 'enemies_destroyed', 'enemies_spotted', 'top_tier', 'DR', 'survived', 'allies_wins', 'enemies_wins'],
'top_tier', 'DR', 'survived', 'allies_wins', 'enemies_wins'],
'team' : [ 'battles', 'win','player_wins','allies_wins','enemies_wins'], 'team' : [ 'battles', 'win','player_wins','allies_wins','enemies_wins'],
'team_extended' : [ 'battles', 'battles%', 'win','player_wins','allies_wins','enemies_wins', 'team_extended' : [ 'battles', 'battles%', 'win','player_wins','allies_wins','enemies_wins', 'player_damage_dealt', 'allies_damage_dealt', 'enemies_damage_dealt', 'player_battles','allies_battles','enemies_battles' ],
'player_damage_dealt', 'allies_damage_dealt', 'enemies_damage_dealt', 'extended' : [ 'battles', 'battles%', 'win','damage_made', 'enemies_destroyed', 'enemies_spotted', 'top_tier', 'DR', 'KDR', 'hit_rate', 'pen_rate', 'survived', 'time_alive%', 'player_wins', 'allies_wins', 'enemies_wins', MISSING_STATS ]
'player_battles','allies_battles','enemies_battles' ],
'extended' : [ 'battles', 'battles%', 'win','damage_made', 'enemies_destroyed', 'enemies_spotted',
'top_tier', 'DR', 'KDR', 'hit_rate', 'pen_rate', 'survived', 'alive',
'player_wins', 'allies_wins', 'enemies_wins', MISSING_STATS ],
'all' : [ cat for cat in _result_fields.keys() ]
} }
_team_fields = [ 'wins', 'battles', 'damage_dealt' ] _team_fields = [ 'wins', 'battles', 'damage_dealt' ]
@@ -953,6 +792,7 @@ class BattleCategory():
avg_fields = set() avg_fields = set()
ratio_fields = set() ratio_fields = set()
RESULT_CAT_HEADER_FRMT = '{:_<20s}'
@classmethod @classmethod
def get_modes(cls) -> list: def get_modes(cls) -> list:
@@ -991,10 +831,6 @@ class BattleCategory():
return cls.result_fields return cls.result_fields
@classmethod
def get_result_fields_all(cls) -> list:
return cls._result_fields.keys()
@classmethod @classmethod
def get_result_fields_ratio(cls, all: bool = False) -> list: def get_result_fields_ratio(cls, all: bool = False) -> list:
if all: if all:
@@ -1053,9 +889,9 @@ class BattleCategory():
@classmethod @classmethod
def print_headers(cls, header: str): def print_headers(cls, cat_name: str):
try: try:
headers = [ header ] headers = [ cls.RESULT_CAT_HEADER_FRMT.format(cat_name) ]
for field in cls.result_fields: for field in cls.result_fields:
print_format = '{:^' + str(cls.get_field_width(field)) + 's}' print_format = '{:^' + str(cls.get_field_width(field)) + 's}'
headers.append(print_format.format(cls.get_field_name(field))) headers.append(print_format.format(cls.get_field_name(field)))
@@ -1067,20 +903,6 @@ class BattleCategory():
return None return None
@classmethod
def get_headers(cls, header: str = None) -> list:
try:
headers = [ header ]
for field in cls.result_fields:
headers.append(cls.get_field_name(field))
return headers
except KeyError as err:
bu.error('Key not found', exception=err)
except Exception as err:
bu.error(exception=err)
return None
def __init__(self): def __init__(self):
try: try:
@@ -1164,22 +986,6 @@ class BattleCategory():
return None return None
def get_results_list(self) -> list:
"""Return results as a list"""
if not self.results_ready:
bu.error('Stats have not been calculated yet. call calc_results() before get_results()')
try:
results = list()
for field in self.result_fields:
results.append(self._result_fields[field][3].format(self.results[field]))
return results
except KeyError as err:
bu.error('Key not found', err)
except Exception as err:
bu.error(exception=err)
return None
def print_results(self): def print_results(self):
if not self.results_ready: if not self.results_ready:
bu.error('Stats have not been calculated yet. call calc_results() before printing()') bu.error('Stats have not been calculated yet. call calc_results() before printing()')
@@ -1197,20 +1003,16 @@ class BattleCategory():
class PlayerHistogram(): class PlayerHistogram():
def __init__(self, field: str, name: str, fields : list, factor: float, cat_format: str ): def __init__(self, field: str, name: str, fields : list, factor: float, format: str ):
self.field = field self.field = field
if self.field == 'wins':
self.name = StatFunc.get_title()
else:
self.name = name self.name = name
self.fields = fields self.fields = fields
self.cat_factor = factor self.cat_factor = factor
self.cat_format = cat_format self.cat_format = format
self.ncat = len(self.fields) - 1 self.ncat = len(self.fields) - 1
self.allies = [0] * self.ncat self.allies = [0] * self.ncat
self.enemies = [0] * self.ncat self.enemies = [0] * self.ncat
self.total = [0] * self.ncat self.total = [0] * self.ncat
self.results = None
def record_ally(self, stat: float): def record_ally(self, stat: float):
for cat in range(0, self.ncat): for cat in range(0, self.ncat):
@@ -1240,7 +1042,7 @@ class PlayerHistogram():
return self.cat_format.format(float(val * self.cat_factor)) return self.cat_format.format(float(val * self.cat_factor))
def calc_results(self): def print(self, ret_json: bool = False):
N_enemies = 0 N_enemies = 0
N_allies = 0 N_allies = 0
res = dict() res = dict()
@@ -1250,7 +1052,10 @@ class PlayerHistogram():
N_allies += self.allies[cat] N_allies += self.allies[cat]
N_total = N_allies + N_enemies 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): 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 ))
if ret_json:
stat = dict() stat = dict()
stat['allies'] = self.allies[cat] stat['allies'] = self.allies[cat]
stat['allies%'] = stat['allies'] / N_allies stat['allies%'] = stat['allies'] / N_allies
@@ -1258,51 +1063,11 @@ class PlayerHistogram():
stat['enemies%'] = stat['enemies'] / N_enemies stat['enemies%'] = stat['enemies'] / N_enemies
stat['total'] = self.allies[cat] + self.enemies[cat] stat['total'] = self.allies[cat] + self.enemies[cat]
stat['total%'] = stat['total'] / N_total stat['total%'] = stat['total'] / N_total
res[self.get_category_name(cat)] = stat res[self.get_category_name(cat)] = stat
self.results = res
return res return res
def get_results(self) -> dict:
return self.results
def get_results_list(self) -> list:
try:
if (self.results != None):
res = list()
res.append([self.name, "Allies", "Allies %", "Enemies", "Enemies %" ,"TOTAL", "TOTAL %"])
for cat in self.results:
stat = self.results[cat]
res.append([ cat, stat['allies'], stat['allies%'],
stat['enemies'], stat['enemies%'],
stat['total'], stat['total%'] ])
return res
else:
bu.error('Results have not been calculated yet.')
return None
except Exception as err:
bu.error(exception=err)
return None
def print(self):
try:
if (self.results != None):
print("\n{:12s} | {:13s} | {:13s} | {:13s}".format(self.name, "Allies", "Enemies", "TOTAL"))
for cat in self.results:
stat = self.results[cat]
print("{:12s} | {:5d} ({:5.1%}) | {:5d} ({:5.1%}) | {:5d} ({:5.1%})".format(cat, stat['allies'], stat['allies%'], stat['enemies'], stat['enemies%'], stat['total'], stat['total%'] ))
else:
bu.error('Results have not been calculated yet.')
except Exception as err:
bu.error(exception=err)
return None
class ErrorCatchingArgumentParser(argparse.ArgumentParser): class ErrorCatchingArgumentParser(argparse.ArgumentParser):
def exit(self, status=0, message=None): def exit(self, status=0, message=None):
if status: if status:
@@ -1314,6 +1079,10 @@ class ErrorCatchingArgumentParser(argparse.ArgumentParser):
## main() ------------------------------------------------------------- ## main() -------------------------------------------------------------
OPT_MODE_DEFAULT = 'default'
OPT_MODE_HELP = 'help'
OPT_MODES = BattleCategory.get_modes() + [OPT_MODE_HELP]
async def main(argv): async def main(argv):
global wg, wi, WG_APP_ID, OPT_MODE_DEFAULT global wg, wi, WG_APP_ID, OPT_MODE_DEFAULT
# set the directory for the script # set the directory for the script
@@ -1321,15 +1090,11 @@ async def main(argv):
os.chdir(os.path.dirname(sys.argv[0])) os.chdir(os.path.dirname(sys.argv[0]))
## Default options: ## Default options:
OPT_MODE_DEFAULT = 'default'
OPT_MODE_HELP = 'help'
OPT_MODES = BattleCategory.get_modes() + [OPT_MODE_HELP]
OPT_DB = False OPT_DB = False
OPT_HIST = False OPT_HIST = False
OPT_STAT_FUNC = StatFunc.get_default() OPT_STAT_FUNC = 'player'
OPT_WORKERS_N = 10 OPT_WORKERS_N = 10
OPT_EXPORT_CSV_FILE = 'export.csv'
OPT_EXPORT_JSON_FILE = 'export.json'
#WG_ACCOUNT = None # format: nick@server, where server is either 'eu', 'ru', 'na', 'asia' or 'china'. #WG_ACCOUNT = None # format: nick@server, where server is either 'eu', 'ru', 'na', 'asia' or 'china'.
# China is not supported since WG API stats are not available there # China is not supported since WG API stats are not available there
@@ -1363,8 +1128,6 @@ async def main(argv):
OPT_HIST = configAnalyzer.getboolean('histograms', OPT_HIST) OPT_HIST = configAnalyzer.getboolean('histograms', OPT_HIST)
OPT_STAT_FUNC = configAnalyzer.get('stat_func', fallback=OPT_STAT_FUNC) OPT_STAT_FUNC = configAnalyzer.get('stat_func', fallback=OPT_STAT_FUNC)
OPT_WORKERS_N = configAnalyzer.getint('workers', OPT_WORKERS_N) OPT_WORKERS_N = configAnalyzer.getint('workers', OPT_WORKERS_N)
OPT_EXPORT_CSV_FILE = configAnalyzer.get('csv_file', OPT_EXPORT_CSV_FILE)
OPT_EXPORT_JSON_FILE = configAnalyzer.get('json_file', OPT_EXPORT_JSON_FILE)
res_categorizations = configAnalyzer.get('categorizations', None) res_categorizations = configAnalyzer.get('categorizations', None)
if res_categorizations != None: if res_categorizations != None:
res_categorizations.replace(' ','') res_categorizations.replace(' ','')
@@ -1407,22 +1170,21 @@ async def main(argv):
parser = ErrorCatchingArgumentParser(description='Analyze Blitz replay JSON files from WoTinspector.com. Use \'upload_wotb_replays.py\' to upload the replay files first.') parser = ErrorCatchingArgumentParser(description='Analyze Blitz replay JSON files from WoTinspector.com. Use \'upload_wotb_replays.py\' to upload the replay files first.')
parser.add_argument('--mode', default=OPT_MODE_DEFAULT, choices=OPT_MODES, help='Select stats to print out (columns). Options: ' + ', '.join(OPT_MODES)) parser.add_argument('--mode', default=OPT_MODE_DEFAULT, choices=OPT_MODES, help='Select stats mode. Options: ' + ', '.join(OPT_MODES))
parser.add_argument('--extra', choices=BattleCategorizationList.get_categorizations_all(), default=None, nargs='*', help='Print extra categories: ' + ', '.join( cat + '=' + BattleCategorizationList.get_categorization_title(cat) for cat in BattleCategorizationList.get_categorizations_all())) parser.add_argument('--extra', choices=BattleCategorizationList.get_categorizations_all(), default=None, nargs='*', help='Print extra categories: ' + ', '.join( cat + '=' + BattleCategorizationList.get_categorization_title(cat) for cat in BattleCategorizationList.get_categorizations_all()))
parser.add_argument('--output', default='plain', choices=['plain', 'db'], help='Select output mode: plain text or database')
parser.add_argument('-id', dest='account_id', type=int, default=None, help='WG account_id to analyze. Replays without the account_id will be ignored.')
parser.add_argument('-a', '--account', type=str, default=None, help='WG account nameto analyze. Format: ACCOUNT_NAME@SERVER')
parser.add_argument('--only_extra', action='store_true', default=False, help='Print only the extra categories') parser.add_argument('--only_extra', action='store_true', default=False, help='Print only the extra categories')
parser.add_argument('--hist', action='store_true', default=OPT_HIST, help='Print player histograms: ' + ', '.join( histogram_fields[k][0] for k in histogram_fields)) parser.add_argument('--hist', action='store_true', default=OPT_HIST, help='Print player histograms: ' + ', '.join( histogram_fields[k][0] for k in histogram_fields))
# parser.add_argument('--output', default='plain', choices=['plain', 'db'], help='Select output mode: plain text or database') parser.add_argument('--stat_func', default=OPT_STAT_FUNC, choices=STAT_FUNC.keys(), help='Select how to calculate for ally/enemy performance: tank-tier stats, global player stats')
parser.add_argument('-id', dest='account_id', type=int, default=None, help='WG account_id to analyze. Replays without the account_id will be ignored.')
parser.add_argument('-a', '--account', type=str, default=None, help='WG account name to analyze. Format: ACCOUNT_NAME@SERVER')
parser.add_argument('--stat_func', default=OPT_STAT_FUNC, choices=StatFunc.get_stat_funcs(), 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') parser.add_argument('-u', '--url', action='store_true', default=False, help='Print replay URLs')
parser.add_argument('--tankfile', type=str, default='tanks.json', help='JSON file to read Tankopedia from. Default is "tanks.json"') 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"') parser.add_argument('--mapfile', type=str, default='maps.json', help='JSON file to read Blitz map names from. Default is "maps.json"')
parser.add_argument('--json', action='store_true', default=False, help='Export data in JSON format') parser.add_argument('-j', '--json', action='store_true', default=False, help='Export data in JSON')
parser.add_argument('--csv', action='store_true', default=False, help='Export data in CSV format')
parser.add_argument('-o','--outfile', type=str, default='-', metavar="OUTPUT", help='File to write results. Default STDOUT') parser.add_argument('-o','--outfile', type=str, default='-', metavar="OUTPUT", help='File to write results. Default STDOUT')
parser.add_argument('--db', action='store_true', default=OPT_DB, help='Use DB - You are unlikely to have it') parser.add_argument('--db', action='store_true', default=OPT_DB, help='Use DB - You are unlikely to have it')
parser.add_argument('--filters', type=str, default=None, help='Filter replays based on categories. Filters given in JSON format.\nUse array "[]" for multiple filters/values. see --mode help.\nExample: : [ {"tier" : [8,9,10] }, { "player_wins" : 5 }]') parser.add_argument('--filters', type=str, default=None, help='Filter replays based on categories. Filters given in JSON format.\nUse array "[]" for multiple filters/values. see --mode help.\nExample: : [ {"tier" : [8,9,20] }, { "player_wins" : 5 }]')
parser.add_argument('--filters_db', type=str, default=None, help='[only for DB setup] Filter replays in DB based on categories. Filters given in MongoDB JSON format. See --mode help') parser.add_argument('--filters_db', type=str, default=None, help='[only for DB setup] Filter replays in DB based on categories. Filters given in MongoDB JSON format. See --mode help')
parser.add_argument('--min', type=int, default=None, help='Only select replays from players with minimum number of replays in the dataset') parser.add_argument('--min', type=int, default=None, help='Only select replays from players with minimum number of replays in the dataset')
parser.add_argument('-d', '--debug', action='store_true', default=False, help='Debug mode') parser.add_argument('-d', '--debug', action='store_true', default=False, help='Debug mode')
@@ -1435,7 +1197,6 @@ async def main(argv):
try: try:
args = parser.parse_args() args = parser.parse_args()
except Exception as err: except Exception as err:
bu.error('Invalid arguments', exception=err)
sys.exit(0) sys.exit(0)
# res_categories = BattleCategorization.get_categorizations(OPT_CATEGORIZATIONS, args) # res_categories = BattleCategorization.get_categorizations(OPT_CATEGORIZATIONS, args)
@@ -1472,8 +1233,6 @@ async def main(argv):
if args.log: if args.log:
await bu.set_file_logging('analyze_wotb_replays', add_timestamp=True) await bu.set_file_logging('analyze_wotb_replays', add_timestamp=True)
StatFunc.set_stat_func(args.stat_func)
# rebase file arguments due to moving the working directory to the script location # rebase file arguments due to moving the working directory to the script location
args.files = bu.rebase_file_args(current_dir, args.files) args.files = bu.rebase_file_args(current_dir, args.files)
@@ -1524,52 +1283,23 @@ async def main(argv):
teamresults = calc_team_stats(results, player_stats, stat_id_map, args) teamresults = calc_team_stats(results, player_stats, stat_id_map, args)
blt_cat_list = process_battle_results(teamresults, args) blt_cat_list = process_battle_results(teamresults, args)
bu.verbose_std('WR = ' + StatFunc.get_title())
blt_cat_list.print_results() blt_cat_list.print_results()
hist = None
histograms = None
if args.hist: if args.hist:
histograms = process_player_dist(results, player_stats, stat_id_map) hist = process_player_dist(results, player_stats, stat_id_map, args.json)
print_player_dist(histograms)
if args.json: if args.json:
res = blt_cat_list.get_results_json() res = blt_cat_list.get_results_json()
if histograms != None: res['histograms'] = hist
res_hist = dict()
for histogram in histograms.keys():
res_hist[histogram] = histograms[histogram].get_results()
res['histograms'] = res_hist
if args.outfile == '-': if args.outfile == '-':
export_file = OPT_EXPORT_JSON_FILE args.outfile = 'export.json'
else: await bu.save_JSON(args.outfile, res, pretty=False) ## Excel does not understand pretty JSON
export_file = args.outfile bu.verbose_std('Data exported to ' + args.outfile)
await bu.save_JSON(export_file, res, pretty=False) ## Excel does not understand pretty JSON
bu.verbose_std('Results exported to: ' + export_file)
if args.csv:
rows = blt_cat_list.get_results_list()
if args.hist:
rows.append('')
rows.append(['Player Histograms'])
for histogram in histograms.values():
rows.append('')
rows.extend(histogram.get_results_list())
if args.outfile == '-':
export_file = OPT_EXPORT_CSV_FILE
else:
export_file = args.outfile
with open(export_file, 'w') as csvfile:
# creating a csv writer object
csvwriter = csv.writer(csvfile)
csvwriter.writerows(rows)
bu.verbose_std('Results exported to: ' + export_file)
bu.debug('Finished. Cleaning up..................') bu.debug('Finished. Cleaning up..................')
except Exception as err: except Exception as err:
bu.error(exception=err) bu.error(exception=err)
except UserWarning as err: except UserWarning as err:
bu.warning(str(err)) bu.warning(str(err))
except asyncio.CancelledError: except asyncio.exceptions.CancelledError:
pass pass
except Exception as err: except Exception as err:
bu.error(exception=err) bu.error(exception=err)
@@ -1620,14 +1350,6 @@ async def help_extended(db : motor.motor_asyncio.AsyncIOMotorDatabase = None, pa
print("\tExample: --filter '{ \"room_type\" :1 }'") print("\tExample: --filter '{ \"room_type\" :1 }'")
print('') print('')
BattleCategorizationList.help() BattleCategorizationList.help()
print('\n-------------------------------------------------------------------')
print('| Rounding errors |')
print('-------------------------------------------------------------------')
print('')
print('Yes, the numbers are rounded UP because reasons.')
print('If interested why please read this: ')
print('\thttps://realpython.com/python-rounding/#pythons-built-in-round-function')
print('No, I am not planning to fix these for now.')
## move the class PlayerHistogram? ## move the class PlayerHistogram?
@@ -1677,7 +1399,7 @@ def filter_min_replays_by_player(results: list, min_replays: int) -> list:
return None return None
def process_player_dist(results: list, player_stats: dict, stat_id_map: dict) -> dict: def process_player_dist(results: list, player_stats: dict, stat_id_map: dict, res_json: bool = False) -> dict:
"""Process player distribution""" """Process player distribution"""
hist_stats = dict() hist_stats = dict()
try: try:
@@ -1685,7 +1407,7 @@ def process_player_dist(results: list, player_stats: dict, stat_id_map: dict) ->
hist_stats[field] = PlayerHistogram(field, histogram_fields[field][0], histogram_fields[field][1], histogram_fields[field][2], histogram_fields[field][3] ) 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 result in results:
for player in result['allies'] | result['enemies']: # union of Sets for player in result['allies'] | result['enemies']: # uniton of Sets
player_remapped = stat_id_map[player] player_remapped = stat_id_map[player]
if player_remapped in player_stats: if player_remapped in player_stats:
if player in result['allies']: if player in result['allies']:
@@ -1695,23 +1417,19 @@ def process_player_dist(results: list, player_stats: dict, stat_id_map: dict) ->
for stat_field in hist_stats: for stat_field in hist_stats:
hist_stats[stat_field].record_enemy(player_stats[player_remapped][stat_field]) hist_stats[stat_field].record_enemy(player_stats[player_remapped][stat_field])
for stat_field in hist_stats: print('\nPlayer Histograms______', end='', flush=True)
hist_stats[stat_field].calc_results()
return hist_stats res = dict()
for stat_field in hist_stats:
res[stat_field] = hist_stats[stat_field].print(res_json)
return res
except Exception as err: except Exception as err:
bu.error(exception=err) bu.error(exception=err)
return None return None
def print_player_dist(histograms: dict):
# Print results
print('\nPlayer Histograms______', end='', flush=True)
for hist in histograms.values():
print('')
hist.print()
def process_battle_results(results: dict, args : argparse.Namespace): def process_battle_results(results: dict, args : argparse.Namespace):
"""Process replay battle results""" """Process replay battle results"""
try: try:
@@ -1787,10 +1505,10 @@ async def process_player_stats(players, N_workers: int, args : argparse.Namespac
try: try:
statsQ = asyncio.Queue() statsQ = asyncio.Queue()
bu.debug('Create player stats queue: ' + str(len(players)) + ' players') bu.debug('Create player stats queue: ' + str(len(players)) + ' players')
stat_id_map = dict() stat_id_map = {}
stat_ids = set() stat_ids = set()
stat_id_map_func = globals()[StatFunc.get_stat_id_func()] stat_id_map_func = globals()[STAT_FUNC[args.stat_func][0]]
for player in players: for player in players:
stat_id_map[player] = stat_id_map_func(player) stat_id_map[player] = stat_id_map_func(player)
@@ -1813,8 +1531,8 @@ async def process_player_stats(players, N_workers: int, args : argparse.Namespac
bu.debug('Cancelling stats workers') bu.debug('Cancelling stats workers')
for task in stats_tasks: for task in stats_tasks:
task.cancel() task.cancel()
player_stats = dict() player_stats = {}
stat_id_remap = dict() stat_id_remap = {}
bu.debug('Gathering stats worker outputs') bu.debug('Gathering stats worker outputs')
for (stats, id_remap) in await asyncio.gather(*stats_tasks): for (stats, id_remap) in await asyncio.gather(*stats_tasks):
@@ -1921,11 +1639,11 @@ def calc_team_stats(results: list, player_stats : dict, stat_id_map : dict, arg
async def stat_worker(queue : asyncio.Queue, workerID: int, args : argparse.Namespace, db : motor.motor_asyncio.AsyncIOMotorDatabase) -> list: async def stat_worker(queue : asyncio.Queue, workerID: int, args : argparse.Namespace, db : motor.motor_asyncio.AsyncIOMotorDatabase) -> list:
"""Worker thread to find stats for player / tank pairs""" """Worker thread to find stats for player / tank pairs"""
# global wg # global wg
stats = dict() stats = {}
stat_id_remap = dict() stat_id_remap = {}
stat_db_func = globals()[StatFunc.get_db_func()] stat_db_func = globals()[STAT_FUNC[args.stat_func][1]]
stat_wg_func = globals()[StatFunc.get_wg_func()] stat_wg_func = globals()[STAT_FUNC[args.stat_func][2]]
bu.debug("workedID: " + str(workerID) + ' started', id=workerID) bu.debug("workedID: " + str(workerID) + ' started', id=workerID)
try: try:
@@ -1971,7 +1689,7 @@ async def stat_worker(queue : asyncio.Queue, workerID: int, args : argparse.Name
bu.error('Unexpected error', err, id=workerID) bu.error('Unexpected error', err, id=workerID)
queue.task_done() queue.task_done()
except (asyncio.CancelledError, concurrent.futures.CancelledError): except (asyncio.CancelledError, concurrent.futures._base.CancelledError):
bu.debug('Stats queue is empty', id=workerID) bu.debug('Stats queue is empty', id=workerID)
except Exception as err: except Exception as err:
bu.error(exception=err, id=workerID) bu.error(exception=err, id=workerID)
@@ -2071,29 +1789,6 @@ async def get_wg_tank_tier_stats(stat_id_str: str, cache_only = False) -> dict:
return None return None
## player stat functions: tier_x
async def get_wg_tier_x_stats(stat_id_str: str, cache_only = False) -> dict:
"""Get player stats from WG. Returns WR per tier of tank_id"""
try:
account_id = int(stat_id_str)
tier_tanks = wg.get_tanks_by_tier(10)
# '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_tank_stats(account_id, tier_tanks, hist_stats, cache_only = cache_only)
#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: 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)""" """Get player stats from MongoDB (you are very unlikely to have it, unless you have set it up)"""
if db == None: if db == None:
@@ -2120,32 +1815,6 @@ async def get_db_tank_tier_stats(db : motor.motor_asyncio.AsyncIOMotorDatabase,
return None return None
async def get_db_tier_x_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, battletime ) = str2ints(stat_id_str)
tier_tanks = wg.get_tanks_by_tier(10)
time_buffer = 2*7*24*3600
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 ## player stat functions: player
async def get_db_player_stats(db : motor.motor_asyncio.AsyncIOMotorDatabase, stat_id_str: str) -> dict: 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)""" """Get player stats from MongoDB (you are very unlikely to have it, unless you have set it up)"""
@@ -2198,7 +1867,7 @@ async def tank_stats_helper(stat_list: list):
try: try:
if stat_list == None: if stat_list == None:
return None return None
stats = dict() stats = {}
hist_fields = histogram_fields.keys() hist_fields = histogram_fields.keys()
if 'battles' not in hist_fields: if 'battles' not in hist_fields:
bu.error('\'battles\' must be defined in \'histogram_fields\'') bu.error('\'battles\' must be defined in \'histogram_fields\'')
@@ -2230,12 +1899,16 @@ async def player_stats_helper(player_stats: dict):
try: try:
if player_stats == None: if player_stats == None:
return None return None
stats = dict() stats = {}
hist_fields = histogram_fields.keys() hist_fields = histogram_fields.keys()
if 'battles' not in hist_fields: if 'battles' not in hist_fields:
bu.error('\'battles\' must be defined in \'histogram_fields\'') bu.error('\'battles\' must be defined in \'histogram_fields\'')
return None return None
# NEEDED 2020-10-25?
#for field in hist_fields:
# stats[field] = 0
for field in hist_fields: for field in hist_fields:
stats[field] = max(0, player_stats['statistics']['all'][field]) stats[field] = max(0, player_stats['statistics']['all'][field])
@@ -2271,7 +1944,7 @@ async def mk_replayQ(queue : asyncio.Queue, args : argparse.Namespace, db : moto
cursor = dbc.find(filters) cursor = dbc.find(filters)
else: else:
# select all # select all
cursor = dbc.find(dict()) cursor = dbc.find({})
bu.debug('Reading replays...') bu.debug('Reading replays...')
async for replay_json in cursor: async for replay_json in cursor:
_id = replay_json['_id'] _id = replay_json['_id']
@@ -2326,12 +1999,9 @@ async def mk_readerQ_item(replay_json, filename : str = None) -> list:
global REPLAY_N global REPLAY_N
REPLAY_N +=1 REPLAY_N +=1
try: try:
if wi.chk_JSON_replay(replay_json):
if not '_id' in replay_json: if not '_id' in replay_json:
id = wi.read_replay_id(replay_json) id = wi.read_replay_id(replay_json)
replay_json['_id'] = id replay_json['_id'] = id
else:
replay_json = None # mark error
except Exception as err: except Exception as err:
bu.error(exception=err) bu.error(exception=err)
if filename == None: if filename == None:
@@ -2355,9 +2025,9 @@ async def replay_reader(queue: asyncio.Queue, readerID: int, args : argparse.Nam
replay_file = item[2] replay_file = item[2]
try: try:
# msg_str = 'Replay[' + str(replayID) + ']: ' msg_str = 'Replay[' + str(replayID) + ']: '
if replay_json == None: if replay_json == None:
bu.warning('Invalid replay. Skipping: ' + (replay_file if replay_file != None else '') ) bu.warning(msg_str + 'Invalid replay. Skipping: ' + (replay_file if replay_file != None else '') )
queue.task_done() queue.task_done()
continue continue
@@ -2366,7 +2036,7 @@ async def replay_reader(queue: asyncio.Queue, readerID: int, args : argparse.Nam
result = await read_replay_JSON(replay_json, args) result = await read_replay_JSON(replay_json, args)
bu.print_progress() bu.print_progress()
if result == None: if result == None:
bu.warning('Invalid replay ' + (replay_file if replay_file != None else '') ) bu.warning(msg_str + 'Invalid replay ' + (replay_file if replay_file != None else '') )
queue.task_done() queue.task_done()
continue continue
@@ -2375,7 +2045,7 @@ async def replay_reader(queue: asyncio.Queue, readerID: int, args : argparse.Nam
playerstanks.update(set([result['player']])) playerstanks.update(set([result['player']]))
results.append(result) results.append(result)
bu.debug('Marking replay [' + str(replayID) + '] done', id=readerID) bu.debug('Marking task ' + str(replayID) + ' done')
except Exception as err: except Exception as err:
bu.error(exception=err) bu.error(exception=err)
@@ -2392,7 +2062,7 @@ async def read_replay_JSON(replay_json: dict, args : argparse.Namespace) -> dict
account_id = args.account_id account_id = args.account_id
url = args.url url = args.url
#db = args.db #db = args.db
result = dict() result = {}
try: try:
# bu.debug(str(replay_json)) # bu.debug(str(replay_json))
if not wi.chk_JSON_replay(replay_json): if not wi.chk_JSON_replay(replay_json):
@@ -2400,10 +2070,9 @@ async def read_replay_JSON(replay_json: dict, args : argparse.Namespace) -> dict
return None return None
result['_id'] = replay_json['_id'] result['_id'] = replay_json['_id']
replay_summary = replay_json['data']['summary'] result['battle_start_timestamp'] = int(replay_json['data']['summary']['battle_start_timestamp'])
result['battle_start_timestamp'] = int(replay_summary['battle_start_timestamp'])
# TBD... # TBD...
protagonist = int(replay_summary['protagonist']) protagonist = int(replay_json['data']['summary']['protagonist'])
# For filtering replays per submitter # For filtering replays per submitter
result['protagonist'] = protagonist result['protagonist'] = protagonist
@@ -2411,17 +2080,17 @@ async def read_replay_JSON(replay_json: dict, args : argparse.Namespace) -> dict
if account_id == None: if account_id == None:
account_id = protagonist account_id = protagonist
elif protagonist != account_id: elif protagonist != account_id:
if account_id in replay_summary['enemies']: if account_id in replay_json['data']['summary']['enemies']:
# switch the teams... # switch the teams...
if replay_summary['battle_result'] != 2: if replay_json['data']['summary']['battle_result'] != 2:
if replay_summary['battle_result'] == 0: if replay_json['data']['summary']['battle_result'] == 0:
replay_summary['battle_result'] = 1 replay_json['data']['summary']['battle_result'] = 1
else: else:
replay_summary['battle_result'] = 0 replay_json['data']['summary']['battle_result'] = 0
tmp = replay_summary['enemies'] tmp = replay_json['data']['summary']['enemies']
replay_summary['enemies'] = replay_summary['allies'] replay_json['data']['summary']['enemies'] = replay_json['data']['summary']['allies']
replay_summary['allies'] = tmp replay_json['data']['summary']['allies'] = tmp
elif account_id in replay_summary['allies']: elif account_id in replay_json['data']['summary']['allies']:
pass pass
else: else:
# looking for an account_id, but account_id not found in the teams => skipping the replay # looking for an account_id, but account_id not found in the teams => skipping the replay
@@ -2430,7 +2099,7 @@ async def read_replay_JSON(replay_json: dict, args : argparse.Namespace) -> dict
result['url'] = replay_json['data']['view_url'] result['url'] = replay_json['data']['view_url']
for key in replay_summary_flds: for key in replay_summary_flds:
result[key] = replay_summary[key] result[key] = replay_json['data']['summary'][key]
except Exception as err: except Exception as err:
bu.error(exception=err) bu.error(exception=err)
@@ -2443,20 +2112,18 @@ async def read_replay_JSON(replay_json: dict, args : argparse.Namespace) -> dict
result['enemies_survived'] = 0 # for steamroller stats result['enemies_survived'] = 0 # for steamroller stats
btl_duration = 0 btl_duration = 0
btl_tier = 0 btl_tier = 0
btl_tier_low = 10
protagonist_tank = None protagonist_tank = None
for player in replay_summary['details']: for player in replay_json['data']['summary']['details']:
btl_duration = max(btl_duration, player['time_alive']) btl_duration = max(btl_duration, player['time_alive'])
player_tank_tier = wg.get_tank_data(player['vehicle_descr'], 'tier') player_tank_tier = wg.get_tank_data(player['vehicle_descr'], 'tier')
btl_tier = max(btl_tier, player_tank_tier) btl_tier = max(btl_tier, player_tank_tier)
btl_tier_low = min(btl_tier_low, player_tank_tier)
if (protagonist != None) and (player['dbid'] == protagonist): if (protagonist != None) and (player['dbid'] == protagonist):
protagonist_tank = player['vehicle_descr'] protagonist_tank = player['vehicle_descr']
if player['dbid'] == account_id: if player['dbid'] == account_id:
# player itself is not put in results['allies'] # player itself is not put in results['allies']
tmp = dict() tmp = {}
tmp['account_id'] = account_id tmp['account_id'] = account_id
tmp['tank_id'] = player['vehicle_descr'] tmp['tank_id'] = player['vehicle_descr']
tmp['tank_tier'] = player_tank_tier tmp['tank_tier'] = player_tank_tier
@@ -2486,7 +2153,7 @@ async def read_replay_JSON(replay_json: dict, args : argparse.Namespace) -> dict
else: else:
survived = False survived = False
if player['dbid'] in replay_summary['allies']: if player['dbid'] in replay_json['data']['summary']['allies']:
result['allies'].add(get_stat_id(tmp_account_id, tmp_tank_id, tmp_battletime)) result['allies'].add(get_stat_id(tmp_account_id, tmp_tank_id, tmp_battletime))
if survived: if survived:
result['allies_survived'] += 1 result['allies_survived'] += 1
@@ -2495,15 +2162,14 @@ async def read_replay_JSON(replay_json: dict, args : argparse.Namespace) -> dict
if survived: if survived:
result['enemies_survived'] += 1 result['enemies_survived'] += 1
result['battle_duration'] = btl_duration
## Rather use 'player' than incomprehensible 'protagonist'... ## Rather use 'player' than incomprehensible 'protagonist'...
result['player'] = get_stat_id(protagonist, protagonist_tank, result['battle_start_timestamp']) result['player'] = get_stat_id(protagonist, protagonist_tank, result['battle_start_timestamp'])
bu.debug('Player stats_id: ' + result['player']) bu.debug('Player stats_id: ' + result['player'])
# remove platoon buddy from stats # remove platoon buddy from stats
if result['squad_index'] != None: if result['squad_index'] != None:
for player in replay_summary['details']: for player in replay_json['data']['summary']['details']:
bu.debug(str(player)) bu.debug(str(player))
if (player['squad_index'] == result['squad_index']) and (player['dbid'] in replay_summary['allies']) and (player['dbid'] != account_id): if (player['squad_index'] == result['squad_index']) and (player['dbid'] in replay_json['data']['summary']['allies']) and (player['dbid'] != account_id):
# platoon buddy found # platoon buddy found
tmp_account_id = player['dbid'] tmp_account_id = player['dbid']
tmp_tank_id = player['vehicle_descr'] tmp_tank_id = player['vehicle_descr']
@@ -2515,13 +2181,12 @@ async def read_replay_JSON(replay_json: dict, args : argparse.Namespace) -> dict
for ratio in BattleCategory.get_result_fields_ratio(all=True): for ratio in BattleCategory.get_result_fields_ratio(all=True):
result[ratio] = BattleCategory.calc_ratio(ratio, result) result[ratio] = BattleCategory.calc_ratio(ratio, result)
result['alive'] = min(result['time_alive'] / result['battle_duration'], 1) result['time_alive%'] = result['time_alive'] / btl_duration
result['battle_tier'] = btl_tier result['battle_tier'] = btl_tier
result['top_tier'] = 1 if (result['tank_tier'] == btl_tier) else 0 result['top_tier'] = 1 if (result['tank_tier'] == btl_tier) else 0
result['win'] = 1 if result['battle_result'] == 1 else 0 result['win'] = 1 if result['battle_result'] == 1 else 0
REPLAY_I += 1 REPLAY_I += 1
result['battle_i'] = REPLAY_I result['battle_i'] = REPLAY_I
result['battle'] = str(REPLAY_I) + ': ' + replay_summary['vehicle'] + ' @' + replay_summary['map_name']
bu.debug(str(result)) bu.debug(str(result))
return result return result
except KeyError as err: except KeyError as err:
+280 -108
View File
@@ -1,5 +1,4 @@
#!/usr/bin/env python3 #!/usr/bin/env python3.8
## PYTHON VERSION MUST BE 3.7 OR HIGHER
import sys, os, json, time, base64, urllib, inspect, hashlib, re, string, random import sys, os, json, time, base64, urllib, inspect, hashlib, re, string, random
import asyncio, aiofiles, aiohttp, aiosqlite, lxml import asyncio, aiofiles, aiohttp, aiosqlite, lxml
@@ -10,10 +9,10 @@ from progress.counter import Counter
from decimal import Decimal from decimal import Decimal
from datetime import datetime from datetime import datetime
MAX_RETRIES = 3 MAX_RETRIES= 3
SLEEP = 1.5 SLEEP = 1.5
LOG_LEVELS = {'silent': 0, 'normal': 1, 'verbose': 2, 'debug': 3} LOG_LEVELS = { 'silent': 0, 'normal': 1, 'verbose': 2, 'debug': 3 }
SILENT = 0 SILENT = 0
NORMAL = 1 NORMAL = 1
VERBOSE = 2 VERBOSE = 2
@@ -22,26 +21,25 @@ _log_level = NORMAL
LOG = False LOG = False
LOGGER = None LOGGER = None
# Progress display ## Progress display
_progress_N = 100 _progress_N = 100
_progress_i = 0 _progress_i = 0
_progress_id = None _progress_id = None
_progress_obj = None _progress_obj = None
UMASK = os.umask(0) UMASK= os.umask(0)
os.umask(UMASK) os.umask(UMASK)
# ----------------------------------------------------------- ## -----------------------------------------------------------
# Class ThrottledClientSession(aiohttp.ClientSession) #### Class ThrottledClientSession(aiohttp.ClientSession)
# ----------------------------------------------------------- ## -----------------------------------------------------------
class ThrottledClientSession(aiohttp.ClientSession): class ThrottledClientSession(aiohttp.ClientSession):
"""Rate-throttled client session class inherited from aiohttp.ClientSession)""" """Rate-throttled client session class inherited from aiohttp.ClientSession)"""
# MIN_SLEEP = 0.1 #MIN_SLEEP = 0.1
def __init__(self, rate_limit: float = None, *args, **kwargs) -> None: def __init__(self, rate_limit: float = None, *args,**kwargs) -> None:
super().__init__(*args, **kwargs) super().__init__(*args,**kwargs)
self.rate_limit = None self.rate_limit = None
self._fillerTask = None self._fillerTask = None
self._queue = None self._queue = None
@@ -56,28 +54,31 @@ class ThrottledClientSession(aiohttp.ClientSession):
self._queue = asyncio.Queue(min(2, int(rate_limit)+1)) self._queue = asyncio.Queue(min(2, int(rate_limit)+1))
self._fillerTask = asyncio.create_task(self._filler()) self._fillerTask = asyncio.create_task(self._filler())
def _get_sleep(self) -> list: def _get_sleep(self) -> list:
if self.rate_limit == None: if self.rate_limit == None:
return None return None
else: else:
return 1/self.rate_limit return 1/self.rate_limit
def get_rate(self) -> float: def get_rate(self) -> float:
"""Return rate of requests""" """Return rate of requests"""
return self._count / (time.time() - self._start_time) return self._count / (time.time() - self._start_time)
def get_stats(self): def get_stats(self):
"""Get session statistics""" """Get session statistics"""
res = {'rate': self.get_rate(), 'rate_limit': self.rate_limit, res = {'rate' : self.get_rate(), 'rate_limit': self.rate_limit, 'count' : self._count }
'count': self._count}
return res return res
def get_stats_str(self): def get_stats_str(self):
"""Print session statistics""" """Print session statistics"""
return 'rate limit: ' + str(self.rate_limit if self.rate_limit != None else '-') + \ return 'rate limit: ' + str(self.rate_limit if self.rate_limit != None else '-') + \
' rate: ' + \ ' rate: ' + "{0:.1f}".format(self.get_rate()) + ' requests: ' + str(self._count)
"{0:.1f}".format(self.get_rate()) + \
' requests: ' + str(self._count)
def reset_counters(self): def reset_counters(self):
"""Reset rate counters and return current results""" """Reset rate counters and return current results"""
@@ -86,12 +87,14 @@ class ThrottledClientSession(aiohttp.ClientSession):
self._count = 0 self._count = 0
return res return res
def set_rate_limit(self, rate_limit: float = 1): def set_rate_limit(self, rate_limit: float = 1):
if rate_limit >= 0: if rate_limit >= 0:
self.rate_limit = rate_limit self.rate_limit = rate_limit
return self.rate_limit return self.rate_limit
return None return None
async def close(self) -> None: async def close(self) -> None:
"""Close rate-limiter's "bucket filler" task""" """Close rate-limiter's "bucket filler" task"""
# DEBUG # DEBUG
@@ -99,11 +102,12 @@ class ThrottledClientSession(aiohttp.ClientSession):
try: try:
if self._fillerTask != None: if self._fillerTask != None:
self._fillerTask.cancel() self._fillerTask.cancel()
await asyncio.wait_for(self._fillerTask, timeout=0.5) await asyncio.wait_for(self._fillerTask, timeout= 0.5)
except (asyncio.CancelledError, asyncio.TimeoutError): except (asyncio.exceptions.CancelledError, asyncio.TimeoutError):
pass pass
await super().close() await super().close()
async def _filler_simple(self): async def _filler_simple(self):
"""Filler task to fill the leaky bucket algo""" """Filler task to fill the leaky bucket algo"""
try: try:
@@ -118,6 +122,7 @@ class ThrottledClientSession(aiohttp.ClientSession):
except Exception as err: except Exception as err:
error(exception=err) error(exception=err)
async def _filler(self): async def _filler(self):
"""Filler task to fill the leaky bucket algo""" """Filler task to fill the leaky bucket algo"""
try: try:
@@ -128,7 +133,7 @@ class ThrottledClientSession(aiohttp.ClientSession):
updated_at = time.monotonic() updated_at = time.monotonic()
fraction = 0 fraction = 0
extra_increment = 0 extra_increment = 0
for i in range(0, self._queue.maxsize): for i in range(0,self._queue.maxsize):
self._queue.put_nowait(i) self._queue.put_nowait(i)
while True: while True:
if not self._queue.full(): if not self._queue.full():
@@ -136,35 +141,35 @@ class ThrottledClientSession(aiohttp.ClientSession):
increment = self.rate_limit * (now - updated_at) increment = self.rate_limit * (now - updated_at)
fraction += increment % 1 fraction += increment % 1
extra_increment = fraction // 1 extra_increment = fraction // 1
items_2_add = int( items_2_add = int(min(self._queue.maxsize - self._queue.qsize(), int(increment) + extra_increment))
min(self._queue.maxsize - self._queue.qsize(), int(increment) + extra_increment))
fraction = fraction % 1 fraction = fraction % 1
for i in range(0, items_2_add): for i in range(0,items_2_add):
self._queue.put_nowait(i) self._queue.put_nowait(i)
updated_at = now updated_at = now
await asyncio.sleep(sleep) await asyncio.sleep(sleep)
except asyncio.CancelledError: except (asyncio.exceptions.CancelledError, asyncio.CancelledError):
debug('Cancelled') debug('Cancelled')
except Exception as err: except Exception as err:
error(exception=err) error(exception=err)
async def _request(self, *args, **kwargs):
async def _request(self, *args,**kwargs):
"""Throttled _request()""" """Throttled _request()"""
if self._queue != None: if self._queue != None:
await self._queue.get() await self._queue.get()
self._queue.task_done() self._queue.task_done()
self._count += 1 self._count += 1
return await super()._request(*args, **kwargs) return await super()._request(*args,**kwargs)
class AsyncLogger(): class AsyncLogger():
"""Async file logger""" """Async file logger"""
def __init__(self) -> None: def __init__(self) -> None:
self._queue = asyncio.Queue() self._queue = asyncio.Queue()
self._task = None self._task = None
self._file = None self._file = None
async def open(self, logfn: str = None): async def open(self, logfn: str = None):
"""Set logging to file""" """Set logging to file"""
if logfn == None: if logfn == None:
@@ -178,6 +183,7 @@ class AsyncLogger():
self._file = None self._file = None
return False return False
async def logger(self): async def logger(self):
"""Async file logger""" """Async file logger"""
if self._file == None: if self._file == None:
@@ -193,12 +199,14 @@ class AsyncLogger():
except Exception as err: except Exception as err:
error(exception=err) error(exception=err)
def log(self, msg: str = ''): def log(self, msg: str = ''):
self._queue.put_nowait(msg) self._queue.put_nowait(msg)
async def close(self): async def close(self):
try: try:
# empty queue & close ## empty queue & close
await self._queue.join() await self._queue.join()
self._task.cancel() self._task.cancel()
self._file.close() self._file.close()
@@ -206,10 +214,9 @@ class AsyncLogger():
error('Error closing log file', err) error('Error closing log file', err)
return None return None
# ----------------------------------------------------------- ## -----------------------------------------------------------
# Utils #### Utils
# ----------------------------------------------------------- ## -----------------------------------------------------------
def set_debug(debug: bool): def set_debug(debug: bool):
global _log_level global _log_level
@@ -247,7 +254,7 @@ def is_silent() -> bool:
return _log_level == SILENT return _log_level == SILENT
def set_log_level(silent: bool, verbose: bool, debug: bool): def set_log_level(silent: bool,verbose: bool, debug: bool):
global _log_level global _log_level
_log_level = NORMAL _log_level = NORMAL
if silent: _log_level = SILENT if silent: _log_level = SILENT
@@ -266,7 +273,7 @@ def get_log_level_str() -> str:
error('Unknown log level: ' + str(_log_level)) error('Unknown log level: ' + str(_log_level))
async def set_file_logging(logfn=None, add_timestamp=False): async def set_file_logging(logfn = None, add_timestamp = False):
"""Set logging to file""" """Set logging to file"""
global LOG, LOGGER global LOG, LOGGER
LOG = True LOG = True
@@ -289,63 +296,47 @@ async def set_file_logging(logfn=None, add_timestamp=False):
async def close_file_logging(): async def close_file_logging():
global LOG, LOGGER global LOG, LOGGER
LOG = False LOG=False
await LOGGER.close() await LOGGER.close()
LOGGER = None LOGGER = None
def _randomword(length): def _randomword(length):
letters = string.ascii_lowercase letters = string.ascii_lowercase
return ''.join(random.choice(letters) for i in range(length)) return ''.join(random.choice(letters) for i in range(length))
def verbose(msg="", id=None) -> bool: def verbose(msg = "", id = None) -> bool:
"""Print a message""" """Print a message"""
return _print_log_msg('', msg, exception=None, id=id, print_msg=(_log_level >= VERBOSE)) return _print_log_msg('', msg, exception=None, id=id, print_msg= (_log_level >= VERBOSE) )
def verbose_std(msg="", id=None) -> bool: def verbose_std(msg = "", id = None) -> bool:
"""Print a message""" """Print a message"""
return _print_log_msg('', msg, exception=None, id=id, print_msg=(_log_level >= NORMAL)) return _print_log_msg('', msg, exception=None, id=id, print_msg= (_log_level >= NORMAL) )
def warning(msg="", id=None, force: bool = False) -> bool: def warning(msg = "", id = None, force: bool = False) -> bool:
"""Print a warning message""" """Print a warning message"""
return _print_log_msg('', 'Warning: ' + msg, None, id, print_msg=(force or (_log_level >= NORMAL))) return _print_log_msg('Warning', msg, None, id, print_msg= (force or (_log_level >= NORMAL)) )
def debug(msg="", id=None, exception=None, force: bool = False) -> bool: def debug(msg = "", id = None, exception = None, force: bool = False) -> bool:
"""print a conditional debug message""" """print a conditional debug message"""
if (_log_level >= DEBUG) or force: if (_log_level >= DEBUG) or force:
return _print_log_msg('DEBUG', msg, exception, id) return _print_log_msg('DEBUG', msg, exception, id)
return False return False
def error(msg="", exception=None, id=None) -> bool: def error(msg = "", exception = None, id = None) -> bool:
"""Print an error message""" """Print an error message"""
return _print_log_msg('ERROR', msg, exception, id) return _print_log_msg('ERROR', msg, exception, id)
def log(msg="", id=None, exception=None) -> bool: def log(msg = "", id = None, exception = None) -> bool:
"""print a conditional debug message""" """print a conditional debug message"""
return _print_log_msg('LOG', msg=msg, exception=exception, id=id, print_msg=(_log_level >= DEBUG)) return _print_log_msg('LOG', msg=msg, exception=exception, id=id, print_msg=(_log_level >= DEBUG))
## Copy with pride: https://stackoverflow.com/questions/2203424/python-how-to-retrieve-class-information-from-a-frame-object
def get_class_from_frame(fr):
args, _, _, value_dict = inspect.getargvalues(fr)
# we check the first parameter for the frame function is
# named 'self'
if len(args) and args[0] == 'self':
# in that case, 'self' will be referenced in value_dict
instance = value_dict.get('self', None)
if instance:
# return its class
return getattr(instance, '__class__', None)
# return None otherwise
return None
def _print_log_msg(prefix = 'LOG', msg = '', exception = None, id = None, print_msg : bool = True): def _print_log_msg(prefix = 'LOG', msg = '', exception = None, id = None, print_msg : bool = True):
# Use empty prefix to determine standard verbose messages # Use empty prefix to determine standard verbose messages
if not (print_msg or LOG): if not (print_msg or LOG):
@@ -354,13 +345,8 @@ def _print_log_msg(prefix = 'LOG', msg = '', exception = None, id = None, print_
if prefix != '': if prefix != '':
curframe = inspect.currentframe() curframe = inspect.currentframe()
calframe = inspect.getouterframes(curframe) calframe = inspect.getouterframes(curframe)
class_name = get_class_from_frame(curframe)
caller = calframe[2].function caller = calframe[2].function
if class_name != None: prefix = prefix + ': ' + caller + '(): '
prefix = prefix + ': ' + class_name + '.' + caller
else:
prefix = prefix + ': ' + caller
prefix = prefix + '(): '
if id != None: if id != None:
prefix = prefix + '[' + str(id) + ']: ' prefix = prefix + '[' + str(id) + ']: '
@@ -548,7 +534,7 @@ async def get_url_JSON(session: aiohttp.ClientSession, url: str, chk_JSON_func =
if url == None: if url == None:
return None return None
# To avoid excessive use of servers ## To avoid excessive use of servers
for retry in range(1,max_tries+1): for retry in range(1,max_tries+1):
try: try:
async with session.get(url) as resp: async with session.get(url) as resp:
@@ -626,9 +612,9 @@ def get_JSON_value(json, key : str = None, keys : list = None, keypath = None):
raise KeyError('Key not found: ' + get_JSON_keypath(keypath, keys[0])) raise KeyError('Key not found: ' + get_JSON_keypath(keypath, keys[0]))
# ----------------------------------------------------------- ## -----------------------------------------------------------
# Class SlowBar #### Class SlowBar
# ----------------------------------------------------------- ## -----------------------------------------------------------
class SlowBar(IncrementalBar): class SlowBar(IncrementalBar):
suffix = '%(index)d/%(max)d %(percent)d%% ETA %(remaining_hours).0f h %(remaining_mins).0f mins' suffix = '%(index)d/%(max)d %(percent)d%% ETA %(remaining_hours).0f h %(remaining_mins).0f mins'
@@ -641,9 +627,9 @@ class SlowBar(IncrementalBar):
return (self.eta - (self.eta // 3600)*3600) // 60 return (self.eta - (self.eta // 3600)*3600) // 60
# ----------------------------------------------------------- ## -----------------------------------------------------------
# Class StatsNotFound #### Class StatsNotFound
# ----------------------------------------------------------- ## -----------------------------------------------------------
class StatsNotFound(Exception): class StatsNotFound(Exception):
pass pass
@@ -651,15 +637,15 @@ class StatsNotFound(Exception):
# ----------------------------------------------------------- ## -----------------------------------------------------------
# Class WG #### Class WG
# ----------------------------------------------------------- ## -----------------------------------------------------------
class WG: class WG:
URL_WG_CLAN_INFO = 'clans/info/?application_id=' URL_WG_CLAN_INFO = 'clans/info/?application_id='
# URL_WG_PLAYER_TANK_LIST = 'tanks/stats/?fields=tank_id%2Clast_battle_time&application_id=' #URL_WG_PLAYER_TANK_LIST = 'tanks/stats/?fields=tank_id%2Clast_battle_time&application_id='
# URL_WG_PLAYER_TANK_LIST = 'tanks/stats/?fields=account_id%2Ctank_id%2Clast_battle_time%2Cbattle_life_time%2Call&application_id=' #URL_WG_PLAYER_TANK_LIST = 'tanks/stats/?fields=account_id%2Ctank_id%2Clast_battle_time%2Cbattle_life_time%2Call&application_id='
URL_WG_PLAYER_TANK_STATS = 'tanks/stats/?application_id=' URL_WG_PLAYER_TANK_STATS = 'tanks/stats/?application_id='
URL_WG_ACCOUNT_ID = 'account/list/?fields=account_id%2Cnickname&application_id=' URL_WG_ACCOUNT_ID = 'account/list/?fields=account_id%2Cnickname&application_id='
URL_WG_PLAYER_STATS = 'account/info/?application_id=' URL_WG_PLAYER_STATS = 'account/info/?application_id='
@@ -732,7 +718,7 @@ class WG:
SQL_PRUNE_CACHE = """DELETE from {} WHERE update_time < {}""" SQL_PRUNE_CACHE = """DELETE from {} WHERE update_time < {}"""
# Default data. Please use the latest maps.json ## Default data. Please use the latest maps.json
maps = { maps = {
"Random": "Random map", "Random": "Random map",
@@ -881,7 +867,7 @@ class WG:
error(exception=err) error(exception=err)
# Class methods ---------------------------------------------------------- ## Class methods ----------------------------------------------------------
@classmethod @classmethod
def get_server(cls, account_id: int) -> str: def get_server(cls, account_id: int) -> str:
@@ -1066,7 +1052,7 @@ class WG:
return False return False
# Methods -------------------------------------------------- ## Methods --------------------------------------------------
def load_tanks(self, tankopedia_fn: str): def load_tanks(self, tankopedia_fn: str):
"""Load tanks from tankopedia JSON""" """Load tanks from tankopedia JSON"""
if tankopedia_fn == None: if tankopedia_fn == None:
@@ -1258,7 +1244,7 @@ class WG:
url = self.get_url_player_tanks_stats(account_id, tank_ids, fields) url = self.get_url_player_tanks_stats(account_id, tank_ids, fields)
json_data = await self.get_url_JSON(url, self.chk_JSON_status) json_data = await self.get_url_JSON(url, self.chk_JSON_status)
if json_data != None: if json_data != None:
# debug('JSON Response received: ' + str(json_data)) #debug('JSON Response received: ' + str(json_data))
stats = json_data['data'][str(account_id)] stats = json_data['data'][str(account_id)]
if cache: if cache:
await self.put_2_statsQ('tank_stats', [account_id, tank_ids], stats) await self.put_2_statsQ('tank_stats', [account_id, tank_ids], stats)
@@ -1271,7 +1257,7 @@ class WG:
async def get_player_stats(self, account_id: int, fields = [], cache=True, cache_only = False) -> dict: async def get_player_stats(self, account_id: int, fields = [], cache=True, cache_only = False) -> dict:
"""Get player's global stats """ """Get player's global stats """
try: try:
# debug('account_id: ' + str(account_id) ) #debug('account_id: ' + str(account_id) )
stats = None stats = None
# try cached stats first: # try cached stats first:
@@ -1291,7 +1277,7 @@ class WG:
url = self.get_url_player_stats(account_id, fields) url = self.get_url_player_stats(account_id, fields)
json_data = await self.get_url_JSON(url, self.chk_JSON_status) json_data = await self.get_url_JSON(url, self.chk_JSON_status)
if json_data != None: if json_data != None:
# debug('JSON Response received: ' + str(json_data)) #debug('JSON Response received: ' + str(json_data))
stats = json_data['data'][str(account_id)] stats = json_data['data'][str(account_id)]
if cache: if cache:
await self.put_2_statsQ('player_stats', [account_id], stats) await self.put_2_statsQ('player_stats', [account_id], stats)
@@ -1329,7 +1315,7 @@ class WG:
url = self.get_url_player_achievements(list(account_ids), fields) url = self.get_url_player_achievements(list(account_ids), fields)
json_data = await self.get_url_JSON(url, self.chk_JSON_status) json_data = await self.get_url_JSON(url, self.chk_JSON_status)
if (json_data != None) and ('data' in json_data): if (json_data != None) and ('data' in json_data):
# debug('JSON Response received: ' + str(json_data)) #debug('JSON Response received: ' + str(json_data))
for account_id in json_data['data'].keys(): for account_id in json_data['data'].keys():
stats[account_id] = json_data['data'][account_id] stats[account_id] = json_data['data'][account_id]
if cache: if cache:
@@ -1457,7 +1443,7 @@ class WG:
return None return None
try: try:
self.cache = await aiosqlite.connect(WG.CACHE_DB_FILE) self.cache = await aiosqlite.connect(WG.CACHE_DB_FILE)
# Create cache tables table ## Create cache tables table
await self.cache.execute(WG.SQL_TANK_STATS_CREATE_TBL) await self.cache.execute(WG.SQL_TANK_STATS_CREATE_TBL)
await self.cache.execute(WG.SQL_PLAYER_STATS_CREATE_TBL) await self.cache.execute(WG.SQL_PLAYER_STATS_CREATE_TBL)
await self.cache.execute(WG.SQL_PLAYER_ACHIEVEMENTS_CREATE_TBL) await self.cache.execute(WG.SQL_PLAYER_ACHIEVEMENTS_CREATE_TBL)
@@ -1488,7 +1474,7 @@ class WG:
else: else:
error('Function to saves stats type \'' + stats_type + '\' is not implemented yet') error('Function to saves stats type \'' + stats_type + '\' is not implemented yet')
except asyncio.CancelledError: except (asyncio.CancelledError):
# this is an eternal loop that will wait until cancelled # this is an eternal loop that will wait until cancelled
return None return None
@@ -1582,13 +1568,13 @@ class WG:
async with self.cache.execute(sql_query, [account_id, NOW() - WG.CACHE_GRACE_TIME] ) as cursor: async with self.cache.execute(sql_query, [account_id, NOW() - WG.CACHE_GRACE_TIME] ) as cursor:
tank_ids = set(tank_ids) tank_ids = set(tank_ids)
async for row in cursor: async for row in cursor:
# debug('account_id: ' + str(account_id) + ': 1') #debug('account_id: ' + str(account_id) + ': 1')
if row[3] == None: if row[3] == None:
# None/null stats found in cache # None/null stats found in cache
# i.e. stats have been requested, but not returned from WG API # i.e. stats have been requested, but not returned from WG API
tank_ids.remove(row[1]) tank_ids.remove(row[1])
continue continue
# debug('account_id: ' + str(account_id) + ': 2') #debug('account_id: ' + str(account_id) + ': 2')
stats.append(json.loads(row[3])) stats.append(json.loads(row[3]))
# debug('account_id: ' + str(account_id) + ': 3') # debug('account_id: ' + str(account_id) + ': 3')
tank_ids.remove(row[1]) tank_ids.remove(row[1])
@@ -1609,15 +1595,15 @@ class WG:
# test for cacheDB existence # test for cacheDB existence
debug('Trying cached stats first') debug('Trying cached stats first')
if self.cache == None: if self.cache == None:
# debug('No cache DB') #debug('No cache DB')
raise StatsNotFound('No cache DB in use') raise StatsNotFound('No cache DB in use')
async with self.cache.execute(WG.SQL_PLAYER_STATS_CACHED, [account_id, NOW() - WG.CACHE_GRACE_TIME] ) as cursor: async with self.cache.execute(WG.SQL_PLAYER_STATS_CACHED, [account_id, NOW() - WG.CACHE_GRACE_TIME] ) as cursor:
row = await cursor.fetchone() row = await cursor.fetchone()
# debug('account_id: ' + str(account_id) + ': 1') #debug('account_id: ' + str(account_id) + ': 1')
if row == None: if row == None:
# no cached stats found, marked with an empty array # no cached stats found, marked with an empty array
# debug('No cached stats found') #debug('No cached stats found')
raise StatsNotFound('No cached stats found') raise StatsNotFound('No cached stats found')
debug('Cached stats found') debug('Cached stats found')
@@ -1641,15 +1627,15 @@ class WG:
# test for cacheDB existence # test for cacheDB existence
debug('Trying cached stats first') debug('Trying cached stats first')
if self.cache == None: if self.cache == None:
# debug('No cache DB') #debug('No cache DB')
raise StatsNotFound('No cache DB in use') raise StatsNotFound('No cache DB in use')
async with self.cache.execute(WG.SQL_PLAYER_ACHIEVEMENTS_CACHED, [account_id, NOW() - WG.CACHE_GRACE_TIME] ) as cursor: async with self.cache.execute(WG.SQL_PLAYER_ACHIEVEMENTS_CACHED, [account_id, NOW() - WG.CACHE_GRACE_TIME] ) as cursor:
row = await cursor.fetchone() row = await cursor.fetchone()
# debug('account_id: ' + str(account_id) + ': 1') #debug('account_id: ' + str(account_id) + ': 1')
if row == None: if row == None:
# no cached stats found, marked with an empty array # no cached stats found, marked with an empty array
# debug('No cached stats found') #debug('No cached stats found')
raise StatsNotFound('No cached stats found') raise StatsNotFound('No cached stats found')
debug('Cached stats found') debug('Cached stats found')
@@ -1668,9 +1654,9 @@ class WG:
return None return None
# ----------------------------------------------------------- ## -----------------------------------------------------------
# Class WoTinspector #### Class WoTinspector
# ----------------------------------------------------------- ## -----------------------------------------------------------
class WoTinspector: class WoTinspector:
URL_WI = 'https://replays.wotinspector.com' URL_WI = 'https://replays.wotinspector.com'
@@ -1753,7 +1739,7 @@ class WoTinspector:
hash.update(data) hash.update(data)
replay_id = hash.hexdigest() replay_id = hash.hexdigest()
# Testing if the replay has already been posted ## Testing if the replay has already been posted
json_resp = await self.get_replay_JSON(replay_id) json_resp = await self.get_replay_JSON(replay_id)
if json_resp != None: if json_resp != None:
debug('Already uploaded: ' + title, id=N) debug('Already uploaded: ' + title, id=N)
@@ -1768,7 +1754,7 @@ class WoTinspector:
} }
url = self.URL_REPLAY_UL + urllib.parse.urlencode(params, quote_via=urllib.parse.quote) url = self.URL_REPLAY_UL + urllib.parse.urlencode(params, quote_via=urllib.parse.quote)
# debug('URL: ' + url) #debug('URL: ' + url)
headers ={'Content-type': 'application/x-www-form-urlencoded'} headers ={'Content-type': 'application/x-www-form-urlencoded'}
payload = { 'file' : (filename, base64.b64encode(data)) } payload = { 'file' : (filename, base64.b64encode(data)) }
except Exception as err: except Exception as err:
@@ -1852,7 +1838,7 @@ class WoTinspector:
# FIX The replay can have allies/enemies i.e. be useful for # FIX The replay can have allies/enemies i.e. be useful for
# team composition analysis, but does not have player contribution # team composition analysis, but does not have player contribution
if ('status' in json_resp) and json_resp['status'] == 'ok' and \ if ('status' in json_resp) and json_resp['status'] == 'ok' and \
(get_JSON_value(json_resp, key='data.summary.winner_team') != None) : (get_JSON_value(json_resp, key='data.summary.allies') != None) :
debug("JSON check OK") debug("JSON check OK")
return True return True
except KeyError as err: except KeyError as err:
@@ -1862,6 +1848,192 @@ class WoTinspector:
return False return False
## -----------------------------------------------------------
#### Class WoTinspector
## -----------------------------------------------------------
class WIReplay:
replayI = 1
STATUS_UNINIT = 'uninitialized'
STATUS_ERROR = 'error'
STATUS_INCOMPLETE = 'teams only'
STATUS_OK = 'OK'
STATUS_PROCESSED = 'processed'
@classmethod
def warning(cls, msg: str = ''):
bu.warning('Replay[' + str(self.ID) + ']: ' + msg + ': ' + (self.filename if self.filename != None else '') )
@classmethod
def check(cls, replay_json) -> str:
status = self.STATUS_ERROR
try:
if ('status' in json_resp) and (json_resp['status'] == 'ok'):
if get_JSON_value(json_resp, key='data.summary.allies') != None :
debug("JSON check OK")
except KeyError as err:
debug('Replay JSON check failed', exception=err)
except:
debug("Replay JSON check failed: " + str(json_resp))
return status
def __init__(self, replay_json, player_id : int = None, filename : str = None):
self.player_id = player_id
self.filename = filename
self.I = self.replayI
self.replayI += 1
self.status = WIReplay.STATUS_UNINIT
self.result = dict()
try:
self.status = self.check(replay_json)
if self.status == self.STATUS_ERROR:
self.warning('Invalid replay. Skipping')
return
self.result['_id'] = replay_json['_id']
self.result['battle_start_timestamp'] = int(replay_json['data']['summary']['battle_start_timestamp'])
protagonist = int(replay_json['data']['summary']['protagonist'])
# For filtering replays per submitter
result['protagonist'] = protagonist
if account_id == None:
account_id = protagonist
elif 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 in replay_json['data']['summary']['allies']:
pass
else:
# looking for an account_id, but account_id not found in the teams => skipping the replay
return None
if url:
result['url'] = replay_json['data']['view_url']
for key in replay_summary_flds:
result[key] = replay_json['data']['summary'][key]
except Exception as err:
bu.error(exception=err)
return None
try:
#bu.debug('Part 2')
result['allies'] = set()
result['enemies'] = set()
result['allies_survived'] = 0 # for steamroller stats
result['enemies_survived'] = 0 # for steamroller stats
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 = {}
tmp['account_id'] = account_id
tmp['tank_id'] = player['vehicle_descr']
tmp['tank_tier'] = player_tank_tier
tmp['tank_name'] = wg.get_tank_name(tmp['tank_id'])
tmp['tank_type'] = wg.get_tank_type_id(tmp['tank_id'])
tmp['tank_nation'] = wg.get_tank_nation_id(tmp['tank_id'])
tmp['is_premium'] = int(wg.is_premium(tmp['tank_id']))
tmp['squad_index'] = player['squad_index']
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
result['allies_survived'] += 1
for key in tmp.keys():
result[key] = tmp[key]
else:
tmp_account_id = player['dbid']
tmp_tank_id = player['vehicle_descr']
tmp_battletime = result['battle_start_timestamp']
if player['death_reason'] == -1:
survived = True
else:
survived = False
if player['dbid'] in replay_json['data']['summary']['allies']:
result['allies'].add(get_stat_id(tmp_account_id, tmp_tank_id, tmp_battletime))
if survived:
result['allies_survived'] += 1
else:
result['enemies'].add(get_stat_id(tmp_account_id, tmp_tank_id, tmp_battletime))
if survived:
result['enemies_survived'] += 1
## 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']:
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 = result['battle_start_timestamp']
# platoon buddy removed from stats
result['allies'].remove(get_stat_id(tmp_account_id, tmp_tank_id, tmp_battletime))
break
for ratio in BattleCategory.get_result_fields_ratio(all=True):
result[ratio] = BattleCategory.calc_ratio(ratio, result)
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
REPLAY_I += 1
result['battle_i'] = REPLAY_I
bu.print_progress()
if result == None:
bu.warning(msg_str + 'Invalid replay ' + (replay_file if replay_file != None else '') )
queue.task_done()
continue
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')
class BlitzStars: class BlitzStars:
URL_BLITZSTARS = 'https://www.blitzstars.com' URL_BLITZSTARS = 'https://www.blitzstars.com'
@@ -1957,13 +2129,13 @@ class BlitzStars:
async def get_player_stats(self, account_id: int, cache=True): async def get_player_stats(self, account_id: int, cache=True):
"""Get player stats from BlitzStars""" """Get player stats from BlitzStars"""
try: try:
# debug('account_id: ' + str(account_id) ) #debug('account_id: ' + str(account_id) )
stats = None stats = None
if cache: if cache:
error('CACHE NOT IMPLEMENTED YET FOR BlitzStars()') error('CACHE NOT IMPLEMENTED YET FOR BlitzStars()')
sys.exit(1) sys.exit(1)
# stats = await self.get_cached_player_stats(account_id,fields) #stats = await self.get_cached_player_stats(account_id,fields)
# stats found unless StatsNotFound exception is raised # stats found unless StatsNotFound exception is raised
return stats return stats
@@ -1977,7 +2149,7 @@ class BlitzStars:
url = self.get_url_player_stats(account_id) url = self.get_url_player_stats(account_id)
stats = await get_url_JSON(self.session, url, self.chk_JSON_player_stats) stats = await get_url_JSON(self.session, url, self.chk_JSON_player_stats)
if stats != None: if stats != None:
# debug('JSON Response received: ' + str(json_data)) #debug('JSON Response received: ' + str(json_data))
if cache: if cache:
error('CACHE NOT IMPLEMENTED YET FOR BlitzStars()') error('CACHE NOT IMPLEMENTED YET FOR BlitzStars()')
sys.exit(1) sys.exit(1)
@@ -1990,13 +2162,13 @@ class BlitzStars:
async def get_player_tank_stats(self, account_id: int, tank_id = None, cache=True): async def get_player_tank_stats(self, account_id: int, tank_id = None, cache=True):
"""Get player stats for all his tanks from BlitzStars""" """Get player stats for all his tanks from BlitzStars"""
try: try:
# debug('account_id: ' + str(account_id) ) #debug('account_id: ' + str(account_id) )
stats = None stats = None
if cache: if cache:
error('CACHE NOT IMPLEMENTED YET FOR BlitzStars()') error('CACHE NOT IMPLEMENTED YET FOR BlitzStars()')
sys.exit(1) sys.exit(1)
# stats = await self.get_cached_player_stats(account_id,fields) #stats = await self.get_cached_player_stats(account_id,fields)
# stats found unless StatsNotFound exception is raised # stats found unless StatsNotFound exception is raised
return stats return stats
@@ -2014,7 +2186,7 @@ class BlitzStars:
url = self.get_url_player_tank_stats(account_id, tank_id) url = self.get_url_player_tank_stats(account_id, tank_id)
stats = await get_url_JSON(self.session, url, self.chk_JSON_tank_stats) stats = await get_url_JSON(self.session, url, self.chk_JSON_tank_stats)
if stats != None: if stats != None:
# debug('JSON Response received: ' + str(json_data)) #debug('JSON Response received: ' + str(json_data))
if cache: if cache:
error('CACHE NOT IMPLEMENTED YET FOR BlitzStars()') error('CACHE NOT IMPLEMENTED YET FOR BlitzStars()')
sys.exit(1) sys.exit(1)
+3 -22
View File
@@ -1,5 +1,4 @@
#!/usr/bin/env python3 #!/usr/bin/env python3.8
## PYTHON VERSION MUST BE 3.7 OR HIGHER
# Extract tankopedia data in WG API JSON format from Blitz app files (Android APK unzipped) # Extract tankopedia data in WG API JSON format from Blitz app files (Android APK unzipped)
@@ -82,7 +81,6 @@ async def main(argv):
tankopedia['data'] = tanks tankopedia['data'] = tanks
tankopedia['userStr'] = userStrs tankopedia['userStr'] = userStrs
bu.verbose_std('New tankopedia \'' + args.tanks + '\' contains ' + str(len(tanks)) + ' tanks') bu.verbose_std('New tankopedia \'' + args.tanks + '\' contains ' + str(len(tanks)) + ' tanks')
bu.verbose_std('New tankopedia \'' + args.tanks + '\' contains ' + str(len(userStrs)) + ' tank strings')
await outfile.write(json.dumps(tankopedia, ensure_ascii=False, indent=4, sort_keys=False)) await outfile.write(json.dumps(tankopedia, ensure_ascii=False, indent=4, sort_keys=False))
if args.maps != None: if args.maps != None:
@@ -114,7 +112,7 @@ async def extract_tanks(blitz_app_base : str, nation: str):
tankList = xmltodict.parse(await f.read()) tankList = xmltodict.parse(await f.read())
for data in tankList['root'].keys(): for data in tankList['root'].keys():
tank_xml = tankList['root'][data] tank_xml = tankList['root'][data]
tank = dict() tank = {}
tank['tank_id'] = await get_tank_id(nation, int(tank_xml['id'])) tank['tank_id'] = await get_tank_id(nation, int(tank_xml['id']))
tank['userStr'] = tank_xml['userString'] tank['userStr'] = tank_xml['userString']
tank['nation'] = nation tank['nation'] = nation
@@ -122,7 +120,6 @@ async def extract_tanks(blitz_app_base : str, nation: str):
#debug(tank_xml['price']) #debug(tank_xml['price'])
tank['is_premium'] = issubclass(type(tank_xml['price']), dict) tank['is_premium'] = issubclass(type(tank_xml['price']), dict)
tank['type'] = await get_tank_type(tank_xml['tags']) tank['type'] = await get_tank_type(tank_xml['tags'])
#bu.debug('Reading tank string: ' + tank['userStr'], force=True)
tanks.append(tank) tanks.append(tank)
except Exception as err: except Exception as err:
bu.error(err) bu.error(err)
@@ -163,25 +160,10 @@ async def convert_tank_names(tanklist : list, tank_strs: dict) -> dict:
try: try:
for tank in tanklist: for tank in tanklist:
tank['name'] = tank_strs[tank['userStr']] tank['name'] = tank_strs[tank['userStr']]
#userStrs[tank['userStr'].split(':')[1]] = tank['name'] userStrs[tank['userStr'].split(':')[1]] = tank['name']
tank.pop('userStr', None) tank.pop('userStr', None)
tankopedia[str(tank['tank_id'])] = tank tankopedia[str(tank['tank_id'])] = tank
for tank_str in tank_strs:
skip = False
key = tank_str.split(':')[1]
bu.debug('Tank string: ' + key + ' = ' + tank_strs[tank_str])
re_strs = [r'^Chassis_', r'^Turret_', r'^_', r'_short$' ]
for re_str in re_strs:
p = re.compile(re_str)
if p.match(key):
skip = True
break
if skip:
continue
userStrs[key] = tank_strs[tank_str]
# sorting # sorting
tankopedia_sorted = collections.OrderedDict() tankopedia_sorted = collections.OrderedDict()
for tank_id in sorted(tankopedia.keys(), key=int): for tank_id in sorted(tankopedia.keys(), key=int):
@@ -190,7 +172,6 @@ async def convert_tank_names(tanklist : list, tank_strs: dict) -> dict:
userStrs_sorted = collections.OrderedDict() userStrs_sorted = collections.OrderedDict()
for userStr in sorted(userStrs.keys()): for userStr in sorted(userStrs.keys()):
userStrs_sorted[userStr] = userStrs[userStr] userStrs_sorted[userStr] = userStrs[userStr]
# bu.debug('Tank strings: ' + str(len(userStrs_sorted)))
except Exception as err: except Exception as err:
bu.error(err) bu.error(err)
-1
View File
@@ -19,7 +19,6 @@
"grossberg": "Dynasty's Pearl", "grossberg": "Dynasty's Pearl",
"himmelsdorf": "Himmelsdorf", "himmelsdorf": "Himmelsdorf",
"holmeisk": "Wasteland", "holmeisk": "Wasteland",
"iceworld": "Everfrost",
"idle": "Yukon", "idle": "Yukon",
"italy": "Vineyards", "italy": "Vineyards",
"karelia": "Rockfield", "karelia": "Rockfield",
-3
View File
@@ -2,9 +2,6 @@
| Release | Date | Notes | Inaccurate? | | Release | Date | Notes | Inaccurate? |
|:-------:|:--------:|-------------|-----| |:-------:|:--------:|-------------|-----|
| 7.7 | 2021-02-24 | Czechoslovakian medium tanks | |
| 7.6 | 2021-01-27 | AMC 13 90, TOG II buffs, free replay camera | |
| 7.5 | 2020-12-09 | ATGM removal, Batchat, Kpz70, T-2020 buff | |
| 7.4 | 2020-11-12 | TD buffs, Clan missions | | | 7.4 | 2020-11-12 | TD buffs, Clan missions | |
| 7.3 | 2020-10-08 | Swedish HT line | | | 7.3 | 2020-10-08 | Swedish HT line | |
| 7.2 | 2020-08-19 | Balance changes, added tier X ammo capa, new aiming reticles | | | 7.2 | 2020-08-19 | Balance changes, added tier X ammo capa, new aiming reticles | |
-172
View File
@@ -1,172 +0,0 @@
,TOTAL,Battles,% Battles,WR,DPB,KPB,Spot,Top tier,DR,KDR,Hit rate,Pen rate,Surv%,Share live,Player WR,Allies WR,Enemies WR,No stats
,Total,6942,100%,71.80%,2058,1.86,1.4,64%,2.3,5.4,86.10%,90.80%,65.30%,90%,57.08%,50.15%,50.12%,0.20%
,,,,,,,,,,,,,,,,,,
,Result,Battles,% Battles,WR,DPB,KPB,Spot,Top tier,DR,KDR,Hit rate,Pen rate,Surv%,Share live,Player WR,Allies WR,Enemies WR,No stats
,Loss,1959,28%,0.00%,1715,1.12,1.3,61%,1.3,1.3,84.70%,88.60%,13.40%,78%,56.47%,49.56%,50.92%,0.10%
,Win,4983,72%,100.00%,2193,2.16,1.4,65%,3,15.1,86.50%,91.40%,85.70%,95%,57.32%,50.39%,49.80%,0.20%
,,,,,,,,,,,,,,,,,,
,Battle Type,Battles,% Battles,WR,DPB,KPB,Spot,Top tier,DR,KDR,Hit rate,Pen rate,Surv%,Share live,Player WR,Allies WR,Enemies WR,No stats
,Encounter,4652,67%,73.00%,1969,1.98,1.4,67%,2.5,6,85.70%,90.70%,66.80%,91%,57.43%,50.41%,50.33%,0.30%
,Supremacy,2290,33%,69.30%,2238,1.63,1.2,60%,2,4.3,87.10%,91.20%,62.20%,89%,56.37%,49.63%,49.69%,0.00%
,,,,,,,,,,,,,,,,,,
,Tier,Battles,% Battles,WR,DPB,KPB,Spot,Top tier,DR,KDR,Hit rate,Pen rate,Surv%,Share live,Player WR,Allies WR,Enemies WR,No stats
,Bottom tier,2480,36%,69.50%,1750,1.53,1.2,0%,2.2,3.9,85.40%,89.20%,60.70%,88%,56.91%,50.13%,49.99%,0.10%
,Top tier,4462,64%,73.10%,2229,2.05,1.5,100%,2.4,6.4,86.50%,91.60%,67.90%,91%,57.18%,50.16%,50.19%,0.20%
,,,,,,,,,,,,,,,,,,
,Tank Tier,Battles,% Battles,WR,DPB,KPB,Spot,Top tier,DR,KDR,Hit rate,Pen rate,Surv%,Share live,Player WR,Allies WR,Enemies WR,No stats
,1,89,1%,80.90%,678,1.89,1.9,40%,3,7.6,86.20%,91.80%,75.30%,95%,55.40%,48.80%,48.40%,0.00%
,2,180,3%,77.80%,970,2.64,1.6,73%,3.3,9.1,82.90%,90.60%,71.10%,93%,56.04%,49.36%,49.46%,0.90%
,3,540,8%,75.70%,1099,2.15,1.4,36%,3.1,7,83.60%,90.00%,69.10%,93%,57.55%,51.15%,50.16%,1.90%
,4,628,9%,75.20%,1256,2.21,1.3,73%,3.1,7.4,85.00%,91.60%,70.20%,94%,55.72%,51.76%,51.65%,0.00%
,5,626,9%,80.20%,1503,2.29,1.3,71%,3.2,8.3,86.90%,89.50%,72.20%,93%,55.69%,50.38%,49.95%,0.00%
,6,1064,15%,74.20%,1801,1.97,1.4,62%,2.4,5.8,86.70%,92.20%,65.90%,90%,56.82%,49.24%,49.31%,0.00%
,7,1141,16%,71.80%,2143,1.69,1.3,50%,2.5,4.9,85.70%,90.10%,65.80%,90%,58.48%,49.62%,49.60%,0.00%
,8,1222,18%,68.20%,2371,1.59,1.4,67%,2.1,4.2,87.40%,90.90%,61.80%,89%,56.84%,49.80%,49.99%,0.00%
,9,598,9%,64.70%,2806,1.61,1.2,49%,2.1,4,89.10%,91.90%,59.50%,87%,56.58%,50.13%,50.32%,0.00%
,10,854,12%,65.60%,3270,1.62,1.5,100%,2,4,88.30%,90.20%,59.60%,88%,58.37%,50.83%,51.16%,0.00%
,,,,,,,,,,,,,,,,,,
,Tank Class,Battles,% Battles,WR,DPB,KPB,Spot,Top tier,DR,KDR,Hit rate,Pen rate,Surv%,Share live,Player WR,Allies WR,Enemies WR,No stats
,Light Tank,1337,19%,74.40%,1589,1.88,1.9,59%,2.3,5.9,83.70%,89.20%,68.40%,90%,57.15%,50.20%,49.90%,0.70%
,Medium Tank,1957,28%,73.10%,2181,1.99,1.7,67%,2.4,5.9,88.20%,91.40%,66.40%,90%,57.23%,50.26%,50.12%,0.10%
,Heavy Tank,2032,29%,70.00%,2180,1.75,1.2,70%,1.9,5,87.20%,90.70%,65.20%,91%,56.72%,50.16%,50.23%,0.00%
,Tank Destroyer,1616,23%,70.30%,2144,1.84,0.7,59%,3,4.8,85.10%,92.60%,61.50%,91%,57.31%,49.98%,50.16%,0.00%
,,,,,,,,,,,,,,,,,,
,Player WR,Battles,% Battles,WR,DPB,KPB,Spot,Top tier,DR,KDR,Hit rate,Pen rate,Surv%,Share live,Player WR,Allies WR,Enemies WR,No stats
,35 - 45%,80,1%,43.80%,1255,0.93,1,55%,1.1,1.5,84.00%,87.10%,37.50%,82%,44.52%,49.84%,51.14%,0.00%
,45 - 50%,584,8%,69.20%,1397,1.46,1.1,62%,1.8,3.8,81.20%,87.40%,61.30%,89%,47.86%,49.84%,49.86%,0.00%
,50 - 55%,1970,28%,69.00%,1861,1.73,1.3,63%,2.1,4.3,85.40%,90.20%,60.20%,88%,53.06%,50.10%,49.99%,0.00%
,55 - 65%,3677,53%,73.50%,2249,2.01,1.4,66%,2.5,6.3,87.50%,91.50%,68.10%,92%,58.78%,50.13%,50.14%,0.00%
,65 - %,631,9%,76.40%,2274,1.95,1.4,61%,2.8,7,84.60%,91.40%,72.10%,91%,69.92%,50.79%,50.51%,1.90%
,,,,,,,,,,,,,,,,,,
,Player Hit Rate,Battles,% Battles,WR,DPB,KPB,Spot,Top tier,DR,KDR,Hit rate,Pen rate,Surv%,Share live,Player WR,Allies WR,Enemies WR,No stats
, 0 - 25%,32,0%,43.80%,1,0,1.6,47%,0,0,0.00%, inf%,25.00%,57%,54.55%,50.15%,51.14%,0.00%
,25 - 50%,47,1%,44.70%,693,0.34,1.3,51%,0.6,0.5,38.70%,90.50%,36.20%,77%,55.05%,50.14%,51.15%,0.00%
,50 - 60%,172,2%,56.40%,876,0.6,1.2,58%,0.8,1.1,54.70%,87.80%,44.80%,81%,56.38%,50.61%,50.48%,0.00%
,60 - 70%,461,7%,58.80%,1254,1.08,1.2,61%,1.4,2.4,65.40%,87.70%,54.20%,87%,56.29%,50.18%,50.50%,0.00%
,70 - 80%,941,14%,70.50%,1691,1.59,1.3,63%,2,4.6,75.50%,89.10%,65.70%,91%,56.92%,50.36%,50.24%,0.00%
,80 - 90%,2091,30%,74.00%,2071,1.98,1.4,66%,2.5,6.3,85.00%,90.60%,68.50%,93%,57.00%,50.10%,50.08%,0.10%
,90 - 95%,1258,18%,81.00%,2561,2.52,1.5,66%,3.3,11,92.30%,92.00%,77.10%,96%,57.73%,50.08%,49.85%,0.30%
,95 - %,1940,28%,69.60%,2259,1.82,1.4,64%,2.2,4.5,99.00%,91.70%,59.80%,86%,57.17%,50.12%,50.11%,0.40%
,,,,,,,,,,,,,,,,,,
,Player Pen Rate,Battles,% Battles,WR,DPB,KPB,Spot,Top tier,DR,KDR,Hit rate,Pen rate,Surv%,Share live,Player WR,Allies WR,Enemies WR,No stats
, 0 - 25%,49,1%,38.80%,6,0,1.5,39%,0,0,45.80%,10.50%,22.40%,56%,55.11%,49.96%,50.47%,0.00%
,25 - 50%,43,1%,30.20%,447,0.19,1.7,44%,0.3,0.2,82.30%,38.40%,20.90%,66%,55.41%,50.75%,51.06%,0.00%
,50 - 60%,110,2%,41.80%,642,0.27,1.5,60%,0.5,0.4,77.50%,53.10%,32.70%,70%,54.44%,50.46%,50.61%,0.00%
,60 - 70%,210,3%,52.40%,1085,0.63,1.3,56%,0.9,1.1,81.00%,65.10%,40.00%,79%,55.71%,50.58%,50.39%,0.00%
,70 - 80%,469,7%,65.00%,1669,1.33,1.4,61%,1.6,3.1,83.50%,75.40%,57.60%,89%,56.39%,50.24%,50.30%,0.00%
,80 - 90%,1544,22%,71.60%,2076,1.86,1.4,61%,2.3,5.5,86.00%,85.30%,66.30%,92%,57.35%,50.17%,50.03%,0.20%
,90 - 95%,1210,17%,79.30%,2509,2.53,1.5,66%,3.3,11,87.80%,92.40%,77.00%,96%,57.57%,50.00%,50.09%,0.20%
,95 - %,3307,48%,73.30%,2100,1.88,1.3,67%,2.4,5.5,86.30%,99.30%,65.60%,90%,57.11%,50.14%,50.10%,0.20%
,,,,,,,,,,,,,,,,,,
Server,account_id_anon,Battles,% Battles,WR,DPB,KPB,Spot,Top tier,DR,KDR,Hit rate,Pen rate,Surv%,Share live,Player WR,Allies WR,Enemies WR,No stats
RU,1,33,0%,78.80%,3112,1.91,1.6,67%,2.5,7.9,89.60%,90.20%,75.80%,87%,71.43%,50.74%,51.07%,0.00%
RU,2,23,0%,87.00%,3525,2.78,1.7,74%,4.3,32,90.40%,94.40%,91.30%,100%,59.39%,48.07%,48.75%,0.00%
RU,3,81,1%,66.70%,1927,1.47,1,75%,1.9,3.1,82.30%,89.60%,53.10%,86%,69.59%,50.23%,50.85%,0.00%
EU,4,26,0%,80.80%,4082,2.62,1.2,62%,4.1,17,91.10%,93.80%,84.60%,98%,53.92%,48.75%,49.31%,0.00%
EU,5,79,1%,59.50%,1920,0.97,1.4,75%,1.4,2,80.20%,87.10%,50.60%,84%,59.06%,50.12%,51.22%,0.00%
EU,6,92,1%,60.90%,1685,0.99,1.5,65%,1.3,1.8,87.80%,88.50%,45.70%,83%,57.79%,50.30%,50.77%,0.00%
EU,7,50,1%,78.00%,2787,2.92,1.9,52%,4,16.2,90.60%,92.70%,82.00%,99%,64.87%,50.49%,50.50%,0.00%
EU,8,69,1%,60.90%,1695,1.48,1.9,55%,1.5,3,87.80%,92.10%,50.70%,77%,54.73%,50.06%,49.81%,0.10%
EU,9,33,0%,63.60%,1529,1.55,1.9,64%,2,3.6,82.10%,91.30%,57.60%,85%,52.48%,49.85%,50.92%,0.00%
EU,10,22,0%,90.90%,2990,3.82,1,59%,3.6,42,85.60%,93.00%,90.90%,100%,57.27%,48.97%,49.67%,0.00%
EU,11,35,1%,62.90%,1197,0.97,1.1,66%,1.3,2,85.20%,88.10%,51.40%,81%,55.89%,50.71%,49.97%,0.00%
EU,12,20,0%,95.00%,4854,3.95,1.2,25%,4.9,79,94.40%,95.60%,95.00%,99%,56.71%,49.15%,49.81%,0.00%
EU,13,80,1%,73.80%,2228,2.65,1.4,69%,3.3,8.5,92.10%,92.70%,68.80%,95%,58.83%,49.43%,49.21%,0.00%
EU,14,30,0%,83.30%,2167,3.93,2,63%,4.2,19.7,87.90%,92.00%,80.00%,98%,62.43%,50.39%,49.69%,0.00%
EU,15,305,4%,55.40%,2126,1.21,1.3,78%,1.6,2.4,85.80%,90.90%,49.50%,87%,58.91%,50.46%,50.47%,0.00%
EU,16,49,1%,69.40%,2362,2.47,1.5,43%,4,7.1,90.20%,93.30%,65.30%,94%,62.94%,49.43%,49.50%,0.00%
EU,17,22,0%,86.40%,1898,4.41,1.4,95%,4.8,48.5,90.60%,94.80%,90.90%,100%,57.61%,48.11%,49.25%,0.00%
EU,18,96,1%,76.00%,1764,2.19,0.9,67%,3.2,7.2,89.40%,95.00%,69.80%,94%,56.39%,50.00%,49.58%,0.00%
EU,19,23,0%,95.70%,1997,2.09,1.3,65%,3.8,16,90.70%,94.20%,87.00%,98%,70.96%,48.51%,49.66%,0.00%
EU,20,256,4%,54.30%,1081,0.58,1.5,55%,0.9,0.8,81.10%,83.80%,28.10%,68%,52.29%,50.31%,49.81%,0.00%
EU,21,73,1%,82.20%,1025,1.9,1.5,55%,2.7,6.3,84.20%,92.30%,69.90%,94%,58.95%,51.61%,50.45%,0.00%
EU,22,53,1%,79.20%,1415,1.98,0.9,47%,3.2,7,87.40%,90.40%,71.70%,95%,57.15%,50.10%,49.76%,0.00%
EU,23,70,1%,81.40%,2224,2.5,1.8,47%,4,9.7,89.20%,90.20%,74.30%,96%,61.99%,50.85%,50.17%,0.00%
EU,24,83,1%,73.50%,3049,2.1,2,73%,3.2,7.2,89.60%,91.70%,71.10%,96%,60.48%,49.78%,49.51%,0.00%
EU,25,127,2%,68.50%,1609,1.86,1.3,52%,2.4,4.5,83.40%,89.50%,59.10%,89%,54.64%,49.76%,49.35%,0.00%
EU,26,52,1%,80.80%,2669,2.62,2.2,50%,3,11.3,87.80%,91.00%,76.90%,95%,62.66%,49.60%,49.65%,0.20%
EU,27,30,0%,86.70%,4246,3.17,1.4,57%,3.6,23.8,88.10%,95.30%,86.70%,99%,56.38%,49.10%,49.54%,0.00%
EU,28,24,0%,83.30%,2823,2.96,1.8,62%,4.9,17.8,89.30%,91.20%,83.30%,98%,64.91%,50.25%,51.06%,0.00%
EU,29,29,0%,75.90%,1750,1.62,1.3,59%,2.2,3.6,88.10%,93.60%,55.20%,91%,54.39%,49.78%,49.34%,0.00%
EU,30,26,0%,92.30%,2631,3.12,1.8,69%,3.1,27,92.90%,93.80%,88.50%,97%,57.57%,49.93%,49.96%,0.00%
EU,31,31,0%,90.30%,2126,2.16,1.2,61%,3.4,13.4,85.20%,91.20%,83.90%,98%,52.31%,50.24%,49.30%,0.00%
EU,32,44,1%,77.30%,1848,2.91,0.7,77%,3.4,14.2,85.40%,91.70%,79.50%,96%,50.22%,50.67%,50.91%,0.00%
EU,33,74,1%,94.60%,1811,3.24,1.5,59%,4.1,48,86.80%,86.30%,93.20%,99%,56.08%,49.98%,49.17%,0.00%
EU,34,158,2%,77.80%,1290,1.66,0.9,53%,2.4,4.4,78.40%,90.70%,62.70%,93%,49.45%,49.80%,50.05%,0.00%
EU,35,21,0%,61.90%,1796,1.19,1.4,86%,1.5,2.5,77.00%,86.60%,52.40%,82%,48.99%,50.98%,50.38%,0.00%
EU,36,120,2%,55.80%,1730,1.1,1.6,68%,1.3,2.2,85.30%,85.90%,49.20%,83%,60.61%,50.43%,49.95%,0.10%
EU,37,25,0%,88.00%,5110,3.6,1.5,96%,3.7,45,94.90%,91.70%,92.00%,100%,60.44%,50.29%,51.16%,0.00%
EU,38,350,5%,62.00%,1159,1.09,1.3,63%,1.2,2.3,83.10%,89.70%,52.30%,84%,52.65%,50.37%,50.42%,0.00%
EU,39,25,0%,84.00%,4965,2.72,1.5,84%,3.6,13.6,89.40%,91.30%,80.00%,97%,54.95%,49.64%,50.59%,0.00%
EU,40,22,0%,90.90%,4394,3.09,2.3,64%,4.2,34,92.60%,90.30%,90.90%,98%,52.49%,50.46%,50.63%,0.00%
EU,41,57,1%,63.20%,3078,1.63,2.3,96%,2,3.3,85.40%,86.30%,50.90%,85%,60.62%,51.59%,52.10%,0.00%
EU,42,22,0%,68.20%,2270,0.95,1.3,91%,1.6,1.9,85.20%,88.90%,50.00%,85%,59.46%,50.97%,50.61%,0.00%
EU,43,45,1%,64.40%,1482,0.93,1.6,62%,1.2,1.8,83.00%,91.60%,48.90%,79%,50.40%,51.15%,50.79%,0.00%
EU,44,56,1%,96.40%,1927,2.79,1.6,77%,5.4,31.2,85.60%,90.20%,91.10%,98%,54.95%,49.96%,49.62%,0.00%
EU,45,28,0%,96.40%,4659,3.5,1.4,64%,3.8,49,89.00%,91.70%,92.90%,100%,54.81%,49.56%,49.85%,0.00%
EU,46,134,2%,70.90%,1291,1.99,1,49%,3.3,5.6,82.90%,91.80%,64.20%,94%,55.71%,51.75%,51.64%,0.00%
EU,47,51,1%,72.50%,1119,1.43,0.9,78%,1.9,3.8,89.20%,89.20%,62.70%,89%,52.58%,52.04%,52.06%,0.00%
EU,48,45,1%,86.70%,2756,2.71,2.2,64%,4.1,12.2,90.10%,91.50%,77.80%,97%,63.68%,49.88%,49.92%,0.00%
EU,49,35,1%,97.10%,5932,3.57,1.1,94%,5.7,41.7,88.10%,96.30%,91.40%,99%,70.46%,49.51%,50.42%,0.00%
EU,50,186,3%,60.80%,1169,1.02,1.2,49%,1.2,2.1,80.20%,89.20%,52.20%,84%,56.65%,50.14%,50.07%,0.00%
EU,51,25,0%,92.00%,2194,4.68,2.3,60%,4.9,117,86.20%,92.00%,96.00%,100%,70.74%,52.26%,51.76%,0.00%
EU,52,93,1%,62.40%,1444,1.1,1.1,54%,1.7,2.9,88.00%,92.30%,62.40%,85%,72.96%,49.89%,50.29%,0.00%
EU,53,25,0%,80.00%,2374,2.8,1.5,68%,3.5,11.7,86.80%,90.50%,76.00%,95%,60.96%,49.42%,48.68%,0.00%
EU,54,30,0%,53.30%,2384,1.27,1.7,73%,1.6,2.5,85.00%,86.50%,50.00%,81%,54.58%,49.33%,50.27%,0.00%
EU,55,29,0%,79.30%,2219,1.93,1.6,69%,2.1,5.1,88.30%,86.10%,62.10%,89%,47.58%,50.43%,49.39%,0.00%
EU,56,73,1%,69.90%,2036,2.33,0.9,64%,3.2,8.5,90.80%,94.00%,72.60%,95%,52.47%,49.24%,49.05%,0.10%
EU,57,26,0%,96.20%,949,3.08,2,50%,3.2,26.7,83.20%,87.10%,88.50%,97%,46.64%,49.42%,48.36%,0.00%
EU,58,20,0%,65.00%,344,0.45,0.9,45%,0.7,1.3,75.50%,73.80%,65.00%,83%,45.36%,49.30%,49.03%,0.00%
EU,59,149,2%,34.90%,1105,0.8,1.1,60%,0.9,1.1,82.80%,85.80%,28.90%,78%,45.21%,50.08%,51.28%,0.00%
EU,60,22,0%,63.60%,1108,0.91,1,91%,1.3,1.4,78.70%,85.00%,36.40%,81%,54.54%,49.66%,49.03%,0.00%
EU,61,36,1%,94.40%,2794,3.81,2.5,83%,6.2,45.7,85.30%,92.10%,91.70%,100%,63.28%,50.37%,50.28%,0.00%
EU,62,32,0%,31.20%,1786,1.19,0.9,75%,1.8,2,90.60%,91.90%,40.60%,88%,61.60%,49.13%,50.35%,0.00%
EU,63,21,0%,85.70%,2476,3.33,2,76%,4.8,35,91.50%,95.70%,90.50%,99%,53.93%,50.29%,49.74%,0.00%
NA,64,23,0%,91.30%,2371,3.57,0.9,70%,4.8,20.5,86.10%,92.50%,82.60%,99%,58.19%,49.00%,50.44%,0.30%
NA,65,25,0%,96.00%,4281,2.56,1.8,72%,4.1,64,90.20%,91.40%,96.00%,100%,62.10%,51.41%,50.28%,0.00%
NA,66,238,3%,76.10%,2183,1.56,1.5,50%,2.8,5.7,79.70%,89.90%,72.70%,92%,68.48%,50.75%,50.57%,0.00%
NA,67,56,1%,73.20%,2022,1.88,1.3,68%,2.5,6.6,85.20%,90.40%,71.40%,93%,55.31%,49.88%,49.73%,0.10%
NA,68,25,0%,92.00%,6141,3.36,1.2,100%,3.5,28,92.20%,93.80%,88.00%,99%,62.64%,49.81%,50.96%,0.00%
NA,69,44,1%,77.30%,4178,1.93,1.5,77%,3,12.1,92.10%,91.60%,84.10%,98%,53.67%,50.20%,50.23%,0.00%
NA,70,26,0%,96.20%,3412,3.46,1.8,69%,3.6,90,92.20%,93.90%,96.20%,100%,65.85%,48.95%,49.25%,0.00%
NA,71,58,1%,77.60%,1642,1.93,0.9,60%,2.2,5.6,86.00%,94.60%,65.50%,87%,46.03%,49.73%,50.08%,0.00%
NA,72,197,3%,78.20%,2416,2.37,1.2,64%,3.3,10.8,86.30%,92.80%,78.20%,97%,56.68%,49.22%,49.50%,0.00%
NA,73,23,0%,60.90%,3932,1.91,2,87%,3,4.9,89.50%,90.70%,60.90%,89%,60.65%,48.75%,50.51%,0.00%
NA,74,42,1%,78.60%,1431,2.45,0.7,36%,2.2,6.9,84.50%,89.30%,64.30%,90%,50.81%,49.26%,49.33%,0.00%
NA,75,21,0%,81.00%,3748,3.29,1.3,57%,4,17.2,91.90%,92.50%,81.00%,100%,61.77%,48.23%,48.98%,0.00%
NA,76,34,0%,88.20%,2707,2.59,1.4,68%,3,44,84.50%,90.50%,94.10%,96%,55.79%,50.34%,49.42%,0.00%
NA,77,115,2%,74.80%,2171,2.19,1.4,74%,2.9,8.4,89.00%,91.40%,73.90%,95%,59.53%,49.65%,49.88%,0.00%
NA,78,23,0%,65.20%,1958,1.96,0.5,61%,2.3,5.6,89.80%,93.30%,65.20%,92%,62.10%,50.31%,49.85%,0.00%
NA,79,218,3%,74.30%,1860,2.16,1.5,65%,2.6,7.2,85.90%,91.60%,70.20%,95%,53.76%,50.11%,49.84%,0.00%
NA,80,35,1%,57.10%,3142,1.71,1.9,80%,2.3,3.2,90.00%,93.10%,45.70%,86%,56.23%,50.16%,51.49%,0.00%
NA,81,32,0%,90.60%,3407,2.62,1.2,62%,3.6,16.8,85.80%,91.90%,84.40%,97%,54.91%,49.20%,49.43%,0.00%
NA,82,20,0%,90.00%,4472,3.4,2.2,75%,4.1,34,86.20%,83.10%,90.00%,100%,50.16%,50.67%,50.02%,0.00%
NA,83,51,1%,72.50%,1023,2.08,1.1,76%,2.2,5.6,88.30%,93.20%,62.70%,91%,73.85%,56.05%,50.69%,23.20%
NA,84,47,1%,80.90%,3820,2.45,1.4,85%,3.7,12.8,93.20%,94.90%,80.90%,98%,61.01%,50.73%,50.39%,0.00%
NA,85,71,1%,91.50%,3159,2.73,0.9,65%,3.9,27.7,89.40%,92.40%,90.10%,99%,55.39%,49.64%,50.01%,0.00%
NA,86,28,0%,85.70%,4249,2.46,1.3,79%,3.1,23,91.90%,93.70%,89.30%,95%,61.07%,50.13%,50.15%,0.00%
NA,87,24,0%,75.00%,3048,2.17,1.9,75%,2.7,10.4,85.40%,90.00%,79.20%,98%,55.74%,49.79%,50.05%,0.00%
NA,88,34,0%,67.60%,3317,2.21,2.1,65%,3.6,6.8,90.50%,91.30%,67.60%,96%,61.83%,49.97%,49.88%,0.00%
NA,89,23,0%,30.40%,1399,0.52,1.3,91%,0.9,0.8,87.30%,85.40%,30.40%,71%,49.58%,50.38%,51.69%,0.00%
NA,90,20,0%,75.00%,1305,0.6,1.4,60%,1.1,1.7,88.10%,80.10%,65.00%,91%,48.23%,49.76%,49.40%,0.00%
NA,91,24,0%,79.20%,2554,2.96,1.2,50%,5.6,17.8,93.30%,95.50%,83.30%,98%,58.79%,50.82%,51.10%,0.00%
NA,92,48,1%,56.20%,1707,0.69,0.7,92%,0.8,1.4,81.30%,86.80%,52.10%,87%,55.85%,53.43%,53.97%,0.00%
NA,93,130,2%,62.30%,1912,1.31,1.3,70%,1.3,2.9,89.00%,91.70%,55.40%,86%,59.63%,49.95%,50.11%,0.00%
NA,94,35,1%,77.10%,3875,2.74,1.8,57%,3.6,12,89.60%,94.40%,77.10%,95%,59.73%,49.60%,50.04%,0.00%
NA,95,23,0%,73.90%,2688,3.17,1,74%,3.4,14.6,92.50%,93.20%,78.30%,96%,51.21%,49.86%,50.33%,0.00%
NA,96,26,0%,61.50%,1796,1.19,1.2,31%,1.6,2.8,75.60%,84.20%,57.70%,91%,54.00%,50.02%,50.03%,0.00%
NA,97,21,0%,90.50%,1869,2.52,2.3,76%,3.2,17.7,85.40%,89.60%,85.70%,96%,56.43%,49.38%,49.01%,0.00%
NA,98,101,1%,71.30%,1499,2,0.7,58%,2.7,5.3,83.30%,91.50%,62.40%,93%,52.60%,49.80%,49.63%,0.10%
NA,99,20,0%,60.00%,1368,1,0.9,45%,1.4,2.9,83.50%,87.90%,65.00%,84%,54.73%,50.49%,50.26%,0.00%
NA,100,24,0%,79.20%,1998,2.54,1.6,75%,3.4,20.3,85.30%,83.70%,87.50%,98%,53.43%,50.95%,50.42%,0.30%
NA,101,26,0%,42.30%,1170,0.77,1.7,73%,1.3,1.2,75.50%,86.50%,34.60%,81%,46.59%,49.31%,50.13%,0.00%
NA,102,26,0%,53.80%,1013,0.85,0.8,62%,1.1,1.2,74.20%,82.40%,26.90%,83%,52.75%,49.08%,49.42%,0.00%
NA,103,23,0%,91.30%,3980,3,1.4,83%,3.1,34.5,86.00%,90.80%,91.30%,97%,63.72%,49.86%,49.82%,0.00%
NA,104,57,1%,71.90%,1694,1.49,0.6,74%,2.8,4.7,82.80%,86.80%,68.40%,92%,46.26%,49.48%,48.84%,0.00%
ASIA,105,330,5%,82.40%,2195,2.01,1.7,63%,2.7,7.5,89.80%,92.70%,73.00%,94%,57.98%,49.95%,49.71%,0.00%
ASIA,106,132,2%,77.30%,1268,1.31,0.8,58%,2.2,3.5,82.20%,92.40%,62.10%,85%,62.78%,49.79%,50.26%,0.00%
ASIA,107,21,0%,90.50%,3749,3.67,1.8,81%,4.1,38.5,90.70%,93.70%,90.50%,97%,53.77%,49.37%,50.06%,0.00%
ASIA,108,35,1%,85.70%,4501,2.6,1.6,74%,4,15.2,89.10%,91.20%,82.90%,99%,54.47%,49.39%,49.24%,0.00%
ASIA,109,67,1%,73.10%,1278,1.7,1.5,54%,2.8,5.7,83.80%,88.00%,70.10%,93%,55.23%,51.01%,50.11%,0.10%
ASIA,110,70,1%,91.40%,1525,1.54,1.1,54%,3.5,54,78.50%,86.10%,97.10%,99%,49.45%,49.53%,48.75%,0.00%
ASIA,111,49,1%,67.30%,2050,1.96,0.7,73%,2.7,5.3,85.70%,91.40%,63.30%,93%,52.58%,49.79%,49.79%,0.00%
1 TOTAL Battles % Battles WR DPB KPB Spot Top tier DR KDR Hit rate Pen rate Surv% Share live Player WR Allies WR Enemies WR No stats
2 Total 6942 100% 71.80% 2058 1.86 1.4 64% 2.3 5.4 86.10% 90.80% 65.30% 90% 57.08% 50.15% 50.12% 0.20%
3
4 Result Battles % Battles WR DPB KPB Spot Top tier DR KDR Hit rate Pen rate Surv% Share live Player WR Allies WR Enemies WR No stats
5 Loss 1959 28% 0.00% 1715 1.12 1.3 61% 1.3 1.3 84.70% 88.60% 13.40% 78% 56.47% 49.56% 50.92% 0.10%
6 Win 4983 72% 100.00% 2193 2.16 1.4 65% 3 15.1 86.50% 91.40% 85.70% 95% 57.32% 50.39% 49.80% 0.20%
7
8 Battle Type Battles % Battles WR DPB KPB Spot Top tier DR KDR Hit rate Pen rate Surv% Share live Player WR Allies WR Enemies WR No stats
9 Encounter 4652 67% 73.00% 1969 1.98 1.4 67% 2.5 6 85.70% 90.70% 66.80% 91% 57.43% 50.41% 50.33% 0.30%
10 Supremacy 2290 33% 69.30% 2238 1.63 1.2 60% 2 4.3 87.10% 91.20% 62.20% 89% 56.37% 49.63% 49.69% 0.00%
11
12 Tier Battles % Battles WR DPB KPB Spot Top tier DR KDR Hit rate Pen rate Surv% Share live Player WR Allies WR Enemies WR No stats
13 Bottom tier 2480 36% 69.50% 1750 1.53 1.2 0% 2.2 3.9 85.40% 89.20% 60.70% 88% 56.91% 50.13% 49.99% 0.10%
14 Top tier 4462 64% 73.10% 2229 2.05 1.5 100% 2.4 6.4 86.50% 91.60% 67.90% 91% 57.18% 50.16% 50.19% 0.20%
15
16 Tank Tier Battles % Battles WR DPB KPB Spot Top tier DR KDR Hit rate Pen rate Surv% Share live Player WR Allies WR Enemies WR No stats
17 1 89 1% 80.90% 678 1.89 1.9 40% 3 7.6 86.20% 91.80% 75.30% 95% 55.40% 48.80% 48.40% 0.00%
18 2 180 3% 77.80% 970 2.64 1.6 73% 3.3 9.1 82.90% 90.60% 71.10% 93% 56.04% 49.36% 49.46% 0.90%
19 3 540 8% 75.70% 1099 2.15 1.4 36% 3.1 7 83.60% 90.00% 69.10% 93% 57.55% 51.15% 50.16% 1.90%
20 4 628 9% 75.20% 1256 2.21 1.3 73% 3.1 7.4 85.00% 91.60% 70.20% 94% 55.72% 51.76% 51.65% 0.00%
21 5 626 9% 80.20% 1503 2.29 1.3 71% 3.2 8.3 86.90% 89.50% 72.20% 93% 55.69% 50.38% 49.95% 0.00%
22 6 1064 15% 74.20% 1801 1.97 1.4 62% 2.4 5.8 86.70% 92.20% 65.90% 90% 56.82% 49.24% 49.31% 0.00%
23 7 1141 16% 71.80% 2143 1.69 1.3 50% 2.5 4.9 85.70% 90.10% 65.80% 90% 58.48% 49.62% 49.60% 0.00%
24 8 1222 18% 68.20% 2371 1.59 1.4 67% 2.1 4.2 87.40% 90.90% 61.80% 89% 56.84% 49.80% 49.99% 0.00%
25 9 598 9% 64.70% 2806 1.61 1.2 49% 2.1 4 89.10% 91.90% 59.50% 87% 56.58% 50.13% 50.32% 0.00%
26 10 854 12% 65.60% 3270 1.62 1.5 100% 2 4 88.30% 90.20% 59.60% 88% 58.37% 50.83% 51.16% 0.00%
27
28 Tank Class Battles % Battles WR DPB KPB Spot Top tier DR KDR Hit rate Pen rate Surv% Share live Player WR Allies WR Enemies WR No stats
29 Light Tank 1337 19% 74.40% 1589 1.88 1.9 59% 2.3 5.9 83.70% 89.20% 68.40% 90% 57.15% 50.20% 49.90% 0.70%
30 Medium Tank 1957 28% 73.10% 2181 1.99 1.7 67% 2.4 5.9 88.20% 91.40% 66.40% 90% 57.23% 50.26% 50.12% 0.10%
31 Heavy Tank 2032 29% 70.00% 2180 1.75 1.2 70% 1.9 5 87.20% 90.70% 65.20% 91% 56.72% 50.16% 50.23% 0.00%
32 Tank Destroyer 1616 23% 70.30% 2144 1.84 0.7 59% 3 4.8 85.10% 92.60% 61.50% 91% 57.31% 49.98% 50.16% 0.00%
33
34 Player WR Battles % Battles WR DPB KPB Spot Top tier DR KDR Hit rate Pen rate Surv% Share live Player WR Allies WR Enemies WR No stats
35 35 - 45% 80 1% 43.80% 1255 0.93 1 55% 1.1 1.5 84.00% 87.10% 37.50% 82% 44.52% 49.84% 51.14% 0.00%
36 45 - 50% 584 8% 69.20% 1397 1.46 1.1 62% 1.8 3.8 81.20% 87.40% 61.30% 89% 47.86% 49.84% 49.86% 0.00%
37 50 - 55% 1970 28% 69.00% 1861 1.73 1.3 63% 2.1 4.3 85.40% 90.20% 60.20% 88% 53.06% 50.10% 49.99% 0.00%
38 55 - 65% 3677 53% 73.50% 2249 2.01 1.4 66% 2.5 6.3 87.50% 91.50% 68.10% 92% 58.78% 50.13% 50.14% 0.00%
39 65 - % 631 9% 76.40% 2274 1.95 1.4 61% 2.8 7 84.60% 91.40% 72.10% 91% 69.92% 50.79% 50.51% 1.90%
40
41 Player Hit Rate Battles % Battles WR DPB KPB Spot Top tier DR KDR Hit rate Pen rate Surv% Share live Player WR Allies WR Enemies WR No stats
42 0 - 25% 32 0% 43.80% 1 0 1.6 47% 0 0 0.00% inf% 25.00% 57% 54.55% 50.15% 51.14% 0.00%
43 25 - 50% 47 1% 44.70% 693 0.34 1.3 51% 0.6 0.5 38.70% 90.50% 36.20% 77% 55.05% 50.14% 51.15% 0.00%
44 50 - 60% 172 2% 56.40% 876 0.6 1.2 58% 0.8 1.1 54.70% 87.80% 44.80% 81% 56.38% 50.61% 50.48% 0.00%
45 60 - 70% 461 7% 58.80% 1254 1.08 1.2 61% 1.4 2.4 65.40% 87.70% 54.20% 87% 56.29% 50.18% 50.50% 0.00%
46 70 - 80% 941 14% 70.50% 1691 1.59 1.3 63% 2 4.6 75.50% 89.10% 65.70% 91% 56.92% 50.36% 50.24% 0.00%
47 80 - 90% 2091 30% 74.00% 2071 1.98 1.4 66% 2.5 6.3 85.00% 90.60% 68.50% 93% 57.00% 50.10% 50.08% 0.10%
48 90 - 95% 1258 18% 81.00% 2561 2.52 1.5 66% 3.3 11 92.30% 92.00% 77.10% 96% 57.73% 50.08% 49.85% 0.30%
49 95 - % 1940 28% 69.60% 2259 1.82 1.4 64% 2.2 4.5 99.00% 91.70% 59.80% 86% 57.17% 50.12% 50.11% 0.40%
50
51 Player Pen Rate Battles % Battles WR DPB KPB Spot Top tier DR KDR Hit rate Pen rate Surv% Share live Player WR Allies WR Enemies WR No stats
52 0 - 25% 49 1% 38.80% 6 0 1.5 39% 0 0 45.80% 10.50% 22.40% 56% 55.11% 49.96% 50.47% 0.00%
53 25 - 50% 43 1% 30.20% 447 0.19 1.7 44% 0.3 0.2 82.30% 38.40% 20.90% 66% 55.41% 50.75% 51.06% 0.00%
54 50 - 60% 110 2% 41.80% 642 0.27 1.5 60% 0.5 0.4 77.50% 53.10% 32.70% 70% 54.44% 50.46% 50.61% 0.00%
55 60 - 70% 210 3% 52.40% 1085 0.63 1.3 56% 0.9 1.1 81.00% 65.10% 40.00% 79% 55.71% 50.58% 50.39% 0.00%
56 70 - 80% 469 7% 65.00% 1669 1.33 1.4 61% 1.6 3.1 83.50% 75.40% 57.60% 89% 56.39% 50.24% 50.30% 0.00%
57 80 - 90% 1544 22% 71.60% 2076 1.86 1.4 61% 2.3 5.5 86.00% 85.30% 66.30% 92% 57.35% 50.17% 50.03% 0.20%
58 90 - 95% 1210 17% 79.30% 2509 2.53 1.5 66% 3.3 11 87.80% 92.40% 77.00% 96% 57.57% 50.00% 50.09% 0.20%
59 95 - % 3307 48% 73.30% 2100 1.88 1.3 67% 2.4 5.5 86.30% 99.30% 65.60% 90% 57.11% 50.14% 50.10% 0.20%
60
61 Server account_id_anon Battles % Battles WR DPB KPB Spot Top tier DR KDR Hit rate Pen rate Surv% Share live Player WR Allies WR Enemies WR No stats
62 RU 1 33 0% 78.80% 3112 1.91 1.6 67% 2.5 7.9 89.60% 90.20% 75.80% 87% 71.43% 50.74% 51.07% 0.00%
63 RU 2 23 0% 87.00% 3525 2.78 1.7 74% 4.3 32 90.40% 94.40% 91.30% 100% 59.39% 48.07% 48.75% 0.00%
64 RU 3 81 1% 66.70% 1927 1.47 1 75% 1.9 3.1 82.30% 89.60% 53.10% 86% 69.59% 50.23% 50.85% 0.00%
65 EU 4 26 0% 80.80% 4082 2.62 1.2 62% 4.1 17 91.10% 93.80% 84.60% 98% 53.92% 48.75% 49.31% 0.00%
66 EU 5 79 1% 59.50% 1920 0.97 1.4 75% 1.4 2 80.20% 87.10% 50.60% 84% 59.06% 50.12% 51.22% 0.00%
67 EU 6 92 1% 60.90% 1685 0.99 1.5 65% 1.3 1.8 87.80% 88.50% 45.70% 83% 57.79% 50.30% 50.77% 0.00%
68 EU 7 50 1% 78.00% 2787 2.92 1.9 52% 4 16.2 90.60% 92.70% 82.00% 99% 64.87% 50.49% 50.50% 0.00%
69 EU 8 69 1% 60.90% 1695 1.48 1.9 55% 1.5 3 87.80% 92.10% 50.70% 77% 54.73% 50.06% 49.81% 0.10%
70 EU 9 33 0% 63.60% 1529 1.55 1.9 64% 2 3.6 82.10% 91.30% 57.60% 85% 52.48% 49.85% 50.92% 0.00%
71 EU 10 22 0% 90.90% 2990 3.82 1 59% 3.6 42 85.60% 93.00% 90.90% 100% 57.27% 48.97% 49.67% 0.00%
72 EU 11 35 1% 62.90% 1197 0.97 1.1 66% 1.3 2 85.20% 88.10% 51.40% 81% 55.89% 50.71% 49.97% 0.00%
73 EU 12 20 0% 95.00% 4854 3.95 1.2 25% 4.9 79 94.40% 95.60% 95.00% 99% 56.71% 49.15% 49.81% 0.00%
74 EU 13 80 1% 73.80% 2228 2.65 1.4 69% 3.3 8.5 92.10% 92.70% 68.80% 95% 58.83% 49.43% 49.21% 0.00%
75 EU 14 30 0% 83.30% 2167 3.93 2 63% 4.2 19.7 87.90% 92.00% 80.00% 98% 62.43% 50.39% 49.69% 0.00%
76 EU 15 305 4% 55.40% 2126 1.21 1.3 78% 1.6 2.4 85.80% 90.90% 49.50% 87% 58.91% 50.46% 50.47% 0.00%
77 EU 16 49 1% 69.40% 2362 2.47 1.5 43% 4 7.1 90.20% 93.30% 65.30% 94% 62.94% 49.43% 49.50% 0.00%
78 EU 17 22 0% 86.40% 1898 4.41 1.4 95% 4.8 48.5 90.60% 94.80% 90.90% 100% 57.61% 48.11% 49.25% 0.00%
79 EU 18 96 1% 76.00% 1764 2.19 0.9 67% 3.2 7.2 89.40% 95.00% 69.80% 94% 56.39% 50.00% 49.58% 0.00%
80 EU 19 23 0% 95.70% 1997 2.09 1.3 65% 3.8 16 90.70% 94.20% 87.00% 98% 70.96% 48.51% 49.66% 0.00%
81 EU 20 256 4% 54.30% 1081 0.58 1.5 55% 0.9 0.8 81.10% 83.80% 28.10% 68% 52.29% 50.31% 49.81% 0.00%
82 EU 21 73 1% 82.20% 1025 1.9 1.5 55% 2.7 6.3 84.20% 92.30% 69.90% 94% 58.95% 51.61% 50.45% 0.00%
83 EU 22 53 1% 79.20% 1415 1.98 0.9 47% 3.2 7 87.40% 90.40% 71.70% 95% 57.15% 50.10% 49.76% 0.00%
84 EU 23 70 1% 81.40% 2224 2.5 1.8 47% 4 9.7 89.20% 90.20% 74.30% 96% 61.99% 50.85% 50.17% 0.00%
85 EU 24 83 1% 73.50% 3049 2.1 2 73% 3.2 7.2 89.60% 91.70% 71.10% 96% 60.48% 49.78% 49.51% 0.00%
86 EU 25 127 2% 68.50% 1609 1.86 1.3 52% 2.4 4.5 83.40% 89.50% 59.10% 89% 54.64% 49.76% 49.35% 0.00%
87 EU 26 52 1% 80.80% 2669 2.62 2.2 50% 3 11.3 87.80% 91.00% 76.90% 95% 62.66% 49.60% 49.65% 0.20%
88 EU 27 30 0% 86.70% 4246 3.17 1.4 57% 3.6 23.8 88.10% 95.30% 86.70% 99% 56.38% 49.10% 49.54% 0.00%
89 EU 28 24 0% 83.30% 2823 2.96 1.8 62% 4.9 17.8 89.30% 91.20% 83.30% 98% 64.91% 50.25% 51.06% 0.00%
90 EU 29 29 0% 75.90% 1750 1.62 1.3 59% 2.2 3.6 88.10% 93.60% 55.20% 91% 54.39% 49.78% 49.34% 0.00%
91 EU 30 26 0% 92.30% 2631 3.12 1.8 69% 3.1 27 92.90% 93.80% 88.50% 97% 57.57% 49.93% 49.96% 0.00%
92 EU 31 31 0% 90.30% 2126 2.16 1.2 61% 3.4 13.4 85.20% 91.20% 83.90% 98% 52.31% 50.24% 49.30% 0.00%
93 EU 32 44 1% 77.30% 1848 2.91 0.7 77% 3.4 14.2 85.40% 91.70% 79.50% 96% 50.22% 50.67% 50.91% 0.00%
94 EU 33 74 1% 94.60% 1811 3.24 1.5 59% 4.1 48 86.80% 86.30% 93.20% 99% 56.08% 49.98% 49.17% 0.00%
95 EU 34 158 2% 77.80% 1290 1.66 0.9 53% 2.4 4.4 78.40% 90.70% 62.70% 93% 49.45% 49.80% 50.05% 0.00%
96 EU 35 21 0% 61.90% 1796 1.19 1.4 86% 1.5 2.5 77.00% 86.60% 52.40% 82% 48.99% 50.98% 50.38% 0.00%
97 EU 36 120 2% 55.80% 1730 1.1 1.6 68% 1.3 2.2 85.30% 85.90% 49.20% 83% 60.61% 50.43% 49.95% 0.10%
98 EU 37 25 0% 88.00% 5110 3.6 1.5 96% 3.7 45 94.90% 91.70% 92.00% 100% 60.44% 50.29% 51.16% 0.00%
99 EU 38 350 5% 62.00% 1159 1.09 1.3 63% 1.2 2.3 83.10% 89.70% 52.30% 84% 52.65% 50.37% 50.42% 0.00%
100 EU 39 25 0% 84.00% 4965 2.72 1.5 84% 3.6 13.6 89.40% 91.30% 80.00% 97% 54.95% 49.64% 50.59% 0.00%
101 EU 40 22 0% 90.90% 4394 3.09 2.3 64% 4.2 34 92.60% 90.30% 90.90% 98% 52.49% 50.46% 50.63% 0.00%
102 EU 41 57 1% 63.20% 3078 1.63 2.3 96% 2 3.3 85.40% 86.30% 50.90% 85% 60.62% 51.59% 52.10% 0.00%
103 EU 42 22 0% 68.20% 2270 0.95 1.3 91% 1.6 1.9 85.20% 88.90% 50.00% 85% 59.46% 50.97% 50.61% 0.00%
104 EU 43 45 1% 64.40% 1482 0.93 1.6 62% 1.2 1.8 83.00% 91.60% 48.90% 79% 50.40% 51.15% 50.79% 0.00%
105 EU 44 56 1% 96.40% 1927 2.79 1.6 77% 5.4 31.2 85.60% 90.20% 91.10% 98% 54.95% 49.96% 49.62% 0.00%
106 EU 45 28 0% 96.40% 4659 3.5 1.4 64% 3.8 49 89.00% 91.70% 92.90% 100% 54.81% 49.56% 49.85% 0.00%
107 EU 46 134 2% 70.90% 1291 1.99 1 49% 3.3 5.6 82.90% 91.80% 64.20% 94% 55.71% 51.75% 51.64% 0.00%
108 EU 47 51 1% 72.50% 1119 1.43 0.9 78% 1.9 3.8 89.20% 89.20% 62.70% 89% 52.58% 52.04% 52.06% 0.00%
109 EU 48 45 1% 86.70% 2756 2.71 2.2 64% 4.1 12.2 90.10% 91.50% 77.80% 97% 63.68% 49.88% 49.92% 0.00%
110 EU 49 35 1% 97.10% 5932 3.57 1.1 94% 5.7 41.7 88.10% 96.30% 91.40% 99% 70.46% 49.51% 50.42% 0.00%
111 EU 50 186 3% 60.80% 1169 1.02 1.2 49% 1.2 2.1 80.20% 89.20% 52.20% 84% 56.65% 50.14% 50.07% 0.00%
112 EU 51 25 0% 92.00% 2194 4.68 2.3 60% 4.9 117 86.20% 92.00% 96.00% 100% 70.74% 52.26% 51.76% 0.00%
113 EU 52 93 1% 62.40% 1444 1.1 1.1 54% 1.7 2.9 88.00% 92.30% 62.40% 85% 72.96% 49.89% 50.29% 0.00%
114 EU 53 25 0% 80.00% 2374 2.8 1.5 68% 3.5 11.7 86.80% 90.50% 76.00% 95% 60.96% 49.42% 48.68% 0.00%
115 EU 54 30 0% 53.30% 2384 1.27 1.7 73% 1.6 2.5 85.00% 86.50% 50.00% 81% 54.58% 49.33% 50.27% 0.00%
116 EU 55 29 0% 79.30% 2219 1.93 1.6 69% 2.1 5.1 88.30% 86.10% 62.10% 89% 47.58% 50.43% 49.39% 0.00%
117 EU 56 73 1% 69.90% 2036 2.33 0.9 64% 3.2 8.5 90.80% 94.00% 72.60% 95% 52.47% 49.24% 49.05% 0.10%
118 EU 57 26 0% 96.20% 949 3.08 2 50% 3.2 26.7 83.20% 87.10% 88.50% 97% 46.64% 49.42% 48.36% 0.00%
119 EU 58 20 0% 65.00% 344 0.45 0.9 45% 0.7 1.3 75.50% 73.80% 65.00% 83% 45.36% 49.30% 49.03% 0.00%
120 EU 59 149 2% 34.90% 1105 0.8 1.1 60% 0.9 1.1 82.80% 85.80% 28.90% 78% 45.21% 50.08% 51.28% 0.00%
121 EU 60 22 0% 63.60% 1108 0.91 1 91% 1.3 1.4 78.70% 85.00% 36.40% 81% 54.54% 49.66% 49.03% 0.00%
122 EU 61 36 1% 94.40% 2794 3.81 2.5 83% 6.2 45.7 85.30% 92.10% 91.70% 100% 63.28% 50.37% 50.28% 0.00%
123 EU 62 32 0% 31.20% 1786 1.19 0.9 75% 1.8 2 90.60% 91.90% 40.60% 88% 61.60% 49.13% 50.35% 0.00%
124 EU 63 21 0% 85.70% 2476 3.33 2 76% 4.8 35 91.50% 95.70% 90.50% 99% 53.93% 50.29% 49.74% 0.00%
125 NA 64 23 0% 91.30% 2371 3.57 0.9 70% 4.8 20.5 86.10% 92.50% 82.60% 99% 58.19% 49.00% 50.44% 0.30%
126 NA 65 25 0% 96.00% 4281 2.56 1.8 72% 4.1 64 90.20% 91.40% 96.00% 100% 62.10% 51.41% 50.28% 0.00%
127 NA 66 238 3% 76.10% 2183 1.56 1.5 50% 2.8 5.7 79.70% 89.90% 72.70% 92% 68.48% 50.75% 50.57% 0.00%
128 NA 67 56 1% 73.20% 2022 1.88 1.3 68% 2.5 6.6 85.20% 90.40% 71.40% 93% 55.31% 49.88% 49.73% 0.10%
129 NA 68 25 0% 92.00% 6141 3.36 1.2 100% 3.5 28 92.20% 93.80% 88.00% 99% 62.64% 49.81% 50.96% 0.00%
130 NA 69 44 1% 77.30% 4178 1.93 1.5 77% 3 12.1 92.10% 91.60% 84.10% 98% 53.67% 50.20% 50.23% 0.00%
131 NA 70 26 0% 96.20% 3412 3.46 1.8 69% 3.6 90 92.20% 93.90% 96.20% 100% 65.85% 48.95% 49.25% 0.00%
132 NA 71 58 1% 77.60% 1642 1.93 0.9 60% 2.2 5.6 86.00% 94.60% 65.50% 87% 46.03% 49.73% 50.08% 0.00%
133 NA 72 197 3% 78.20% 2416 2.37 1.2 64% 3.3 10.8 86.30% 92.80% 78.20% 97% 56.68% 49.22% 49.50% 0.00%
134 NA 73 23 0% 60.90% 3932 1.91 2 87% 3 4.9 89.50% 90.70% 60.90% 89% 60.65% 48.75% 50.51% 0.00%
135 NA 74 42 1% 78.60% 1431 2.45 0.7 36% 2.2 6.9 84.50% 89.30% 64.30% 90% 50.81% 49.26% 49.33% 0.00%
136 NA 75 21 0% 81.00% 3748 3.29 1.3 57% 4 17.2 91.90% 92.50% 81.00% 100% 61.77% 48.23% 48.98% 0.00%
137 NA 76 34 0% 88.20% 2707 2.59 1.4 68% 3 44 84.50% 90.50% 94.10% 96% 55.79% 50.34% 49.42% 0.00%
138 NA 77 115 2% 74.80% 2171 2.19 1.4 74% 2.9 8.4 89.00% 91.40% 73.90% 95% 59.53% 49.65% 49.88% 0.00%
139 NA 78 23 0% 65.20% 1958 1.96 0.5 61% 2.3 5.6 89.80% 93.30% 65.20% 92% 62.10% 50.31% 49.85% 0.00%
140 NA 79 218 3% 74.30% 1860 2.16 1.5 65% 2.6 7.2 85.90% 91.60% 70.20% 95% 53.76% 50.11% 49.84% 0.00%
141 NA 80 35 1% 57.10% 3142 1.71 1.9 80% 2.3 3.2 90.00% 93.10% 45.70% 86% 56.23% 50.16% 51.49% 0.00%
142 NA 81 32 0% 90.60% 3407 2.62 1.2 62% 3.6 16.8 85.80% 91.90% 84.40% 97% 54.91% 49.20% 49.43% 0.00%
143 NA 82 20 0% 90.00% 4472 3.4 2.2 75% 4.1 34 86.20% 83.10% 90.00% 100% 50.16% 50.67% 50.02% 0.00%
144 NA 83 51 1% 72.50% 1023 2.08 1.1 76% 2.2 5.6 88.30% 93.20% 62.70% 91% 73.85% 56.05% 50.69% 23.20%
145 NA 84 47 1% 80.90% 3820 2.45 1.4 85% 3.7 12.8 93.20% 94.90% 80.90% 98% 61.01% 50.73% 50.39% 0.00%
146 NA 85 71 1% 91.50% 3159 2.73 0.9 65% 3.9 27.7 89.40% 92.40% 90.10% 99% 55.39% 49.64% 50.01% 0.00%
147 NA 86 28 0% 85.70% 4249 2.46 1.3 79% 3.1 23 91.90% 93.70% 89.30% 95% 61.07% 50.13% 50.15% 0.00%
148 NA 87 24 0% 75.00% 3048 2.17 1.9 75% 2.7 10.4 85.40% 90.00% 79.20% 98% 55.74% 49.79% 50.05% 0.00%
149 NA 88 34 0% 67.60% 3317 2.21 2.1 65% 3.6 6.8 90.50% 91.30% 67.60% 96% 61.83% 49.97% 49.88% 0.00%
150 NA 89 23 0% 30.40% 1399 0.52 1.3 91% 0.9 0.8 87.30% 85.40% 30.40% 71% 49.58% 50.38% 51.69% 0.00%
151 NA 90 20 0% 75.00% 1305 0.6 1.4 60% 1.1 1.7 88.10% 80.10% 65.00% 91% 48.23% 49.76% 49.40% 0.00%
152 NA 91 24 0% 79.20% 2554 2.96 1.2 50% 5.6 17.8 93.30% 95.50% 83.30% 98% 58.79% 50.82% 51.10% 0.00%
153 NA 92 48 1% 56.20% 1707 0.69 0.7 92% 0.8 1.4 81.30% 86.80% 52.10% 87% 55.85% 53.43% 53.97% 0.00%
154 NA 93 130 2% 62.30% 1912 1.31 1.3 70% 1.3 2.9 89.00% 91.70% 55.40% 86% 59.63% 49.95% 50.11% 0.00%
155 NA 94 35 1% 77.10% 3875 2.74 1.8 57% 3.6 12 89.60% 94.40% 77.10% 95% 59.73% 49.60% 50.04% 0.00%
156 NA 95 23 0% 73.90% 2688 3.17 1 74% 3.4 14.6 92.50% 93.20% 78.30% 96% 51.21% 49.86% 50.33% 0.00%
157 NA 96 26 0% 61.50% 1796 1.19 1.2 31% 1.6 2.8 75.60% 84.20% 57.70% 91% 54.00% 50.02% 50.03% 0.00%
158 NA 97 21 0% 90.50% 1869 2.52 2.3 76% 3.2 17.7 85.40% 89.60% 85.70% 96% 56.43% 49.38% 49.01% 0.00%
159 NA 98 101 1% 71.30% 1499 2 0.7 58% 2.7 5.3 83.30% 91.50% 62.40% 93% 52.60% 49.80% 49.63% 0.10%
160 NA 99 20 0% 60.00% 1368 1 0.9 45% 1.4 2.9 83.50% 87.90% 65.00% 84% 54.73% 50.49% 50.26% 0.00%
161 NA 100 24 0% 79.20% 1998 2.54 1.6 75% 3.4 20.3 85.30% 83.70% 87.50% 98% 53.43% 50.95% 50.42% 0.30%
162 NA 101 26 0% 42.30% 1170 0.77 1.7 73% 1.3 1.2 75.50% 86.50% 34.60% 81% 46.59% 49.31% 50.13% 0.00%
163 NA 102 26 0% 53.80% 1013 0.85 0.8 62% 1.1 1.2 74.20% 82.40% 26.90% 83% 52.75% 49.08% 49.42% 0.00%
164 NA 103 23 0% 91.30% 3980 3 1.4 83% 3.1 34.5 86.00% 90.80% 91.30% 97% 63.72% 49.86% 49.82% 0.00%
165 NA 104 57 1% 71.90% 1694 1.49 0.6 74% 2.8 4.7 82.80% 86.80% 68.40% 92% 46.26% 49.48% 48.84% 0.00%
166 ASIA 105 330 5% 82.40% 2195 2.01 1.7 63% 2.7 7.5 89.80% 92.70% 73.00% 94% 57.98% 49.95% 49.71% 0.00%
167 ASIA 106 132 2% 77.30% 1268 1.31 0.8 58% 2.2 3.5 82.20% 92.40% 62.10% 85% 62.78% 49.79% 50.26% 0.00%
168 ASIA 107 21 0% 90.50% 3749 3.67 1.8 81% 4.1 38.5 90.70% 93.70% 90.50% 97% 53.77% 49.37% 50.06% 0.00%
169 ASIA 108 35 1% 85.70% 4501 2.6 1.6 74% 4 15.2 89.10% 91.20% 82.90% 99% 54.47% 49.39% 49.24% 0.00%
170 ASIA 109 67 1% 73.10% 1278 1.7 1.5 54% 2.8 5.7 83.80% 88.00% 70.10% 93% 55.23% 51.01% 50.11% 0.10%
171 ASIA 110 70 1% 91.40% 1525 1.54 1.1 54% 3.5 54 78.50% 86.10% 97.10% 99% 49.45% 49.53% 48.75% 0.00%
172 ASIA 111 49 1% 67.30% 2050 1.96 0.7 73% 2.7 5.3 85.70% 91.40% 63.30% 93% 52.58% 49.79% 49.79% 0.00%
+2049 -3733
View File
File diff suppressed because it is too large. Load diff
+1 -2
View File
@@ -1,5 +1,4 @@
#!/usr/bin/env python3 #!/usr/bin/env python3.8
## PYTHON VERSION MUST BE 3.7 OR HIGHER
import json, argparse, inspect, sys, os, io, base64, aiohttp, urllib, asyncio, aiofiles, aioconsole import json, argparse, inspect, sys, os, io, base64, aiohttp, urllib, asyncio, aiofiles, aioconsole
import logging, re, concurrent.futures, configparser, motor.motor_asyncio, ssl, zipfile import logging, re, concurrent.futures, configparser, motor.motor_asyncio, ssl, zipfile