Author SHA1 Message Date
Jylpah 7e39fdc442 Fix --filters bug with number categorization 2021-11-23 09:46:04 +02:00
Jylpah 14ee570ee5 Merge branch 'master' of https://github.com/Jylpah/blitz-tools 2021-10-29 21:18:27 +03:00
Jylpah e0228a4bf1 Fix shebang 2021-10-29 21:18:24 +03:00
Jylpah 2a93cfbce3 update 8.4 2021-10-24 21:19:06 +03:00
Jylpah 78a959224c Update 8.4 2021-10-23 22:07:23 +03:00
Jylpah ae24ab35f7 Updates 8.0 - 8.3 added 2021-09-17 10:44:38 +03:00
Jylpah ba6b4781e5 Update 8.3 2021-09-16 17:31:10 +03:00
Jylpah 050bd99488 get_filter_categories(): fix, add room_type=5: Quick Tournament(?) 2021-09-10 21:03:41 +03:00
Jylpah eb39c9ce84 Update 8.2 2021-08-22 21:21:42 +03:00
Jylpah 24547763e9 replayWorker(): Better warning text to incomplete replays 2021-08-13 13:43:17 +03:00
Jylpah 28347e1653 Fix printing of player/allies/enemies average damage columns 2021-08-07 16:42:19 +03:00
Jylpah ffe5afbf1e getTitle(): More robust tank names based on tank_ids, not UsrStrs that keep changing. 2021-08-07 16:40:42 +03:00
Jylpah 17ac09afde Update analyze_wotb_replays.py 2021-08-07 16:26:25 +03:00
Jylpah 9117963085 filter_results() bug fix 2021-08-03 19:14:44 +03:00
Jylpah dc8f9a233d Merge pull request #32 from Jylpah/master-platoon
help_extended(): change order of filter helps
2021-08-03 12:43:59 +03:00
Jylpah ce3f9993c5 help_extended(): change order of filter helps 2021-08-03 12:43:43 +03:00
Jylpah fd7eb8261d Merge pull request #31 from Jylpah/master-platoon
Added platoon rate and categorization by platoon/solo games
2021-08-03 12:43:19 +03:00
Jylpah 748ed7806b Added platoon rate and categorization by platoon/solo games 2021-08-03 12:40:36 +03:00
Jylpah 1ada279728 Update Lupus tanks.json 2021-07-27 18:47:08 +03:00
Jylpah 57cbc5b233 Tanks sorted by tank_id 2021-07-15 20:22:46 +03:00
Jylpah 43842dc140 Update 8.1 2021-07-15 20:21:53 +03:00
Jylpah 170a801652 sort_dict(): added 2021-07-15 20:21:32 +03:00
Jylpah 210e943157 Update 8.0 2021-06-08 16:57:42 +03:00
Jylpah b3b7b81eb4 BattleCategory(): add 'hits_bounced' result field 2021-06-03 15:02:27 +03:00
Jylpah 446e289041 Add --mode dmg_received, improved --mode help, new fields 2021-06-03 14:48:17 +03:00
Jylpah f8a0fdfbe1 Merge pull request #30 from Jylpah/stable-export-replays
Stable export replays
2021-05-12 00:16:42 +03:00
Jylpah 4c0de49077 name2tank_str(): error() -> debug() 2021-05-12 00:05:52 +03:00
Jylpah 07aa7af32f bug fix 2021-05-12 00:04:36 +03:00
Jylpah ea3306ee6c WoTinspector.post_replay(): add _id 2021-05-12 00:04:21 +03:00
Jylpah e4882d70af export replays(): --replays option to export replays 2021-05-11 20:55:13 +03:00
Jylpah 433c26d2f8 get_date_str(), get_replay_filename(): added 2021-05-11 20:54:39 +03:00
Jylpah f6b1b1353f Update analyze_wotb_replays.py 2021-05-11 14:14:10 +03:00
8 changed files with 727 additions and 198 deletions

No files matched your search

+182 -68
View File
@@ -28,6 +28,8 @@ REPLAY_I = 0
STAT_TANK_BATTLE_MIN = 100 STAT_TANK_BATTLE_MIN = 100
BATTLE_TIME_BUCKET = 3600*24*14 BATTLE_TIME_BUCKET = 3600*24*14
OPT_EXPORT_REPLAY_DIR = 'export_replays'
RE_SRC_IS_DB = re.compile(r'^DB:') RE_SRC_IS_DB = re.compile(r'^DB:')
class StatFunc: class StatFunc:
@@ -98,7 +100,7 @@ replay_summary_flds = [
'protagonist', 'protagonist',
'player_name', 'player_name',
'title', 'title',
'mastery_badge' 'mastery_badge'
] ]
replay_details_flds = [ replay_details_flds = [
@@ -165,6 +167,7 @@ class BattleCategorizationList():
'Regular' : 1, 'Regular' : 1,
'Training Room' : 2, 'Training Room' : 2,
'Tournament' : 4, 'Tournament' : 4,
'Quick Tournament' : 5, # maybe?
'Rating' : 7, 'Rating' : 7,
'Mad Games' : 8, 'Mad Games' : 8,
'Realistic Battles' : 22, 'Realistic Battles' : 22,
@@ -182,6 +185,7 @@ class BattleCategorizationList():
'battle_type' : [ 'Battle Type', 'category', [ 'Encounter', 'Supremacy']], 'battle_type' : [ 'Battle Type', 'category', [ 'Encounter', 'Supremacy']],
'room_type' : [ 'Battle Mode', 'category', _BATTLE_MODES ], 'room_type' : [ 'Battle Mode', 'category', _BATTLE_MODES ],
'top_tier' : [ 'Tier', 'category', [ 'Bottom tier', 'Top tier']], 'top_tier' : [ 'Tier', 'category', [ 'Bottom tier', 'Top tier']],
'in_platoon' : [ 'Platoon', 'category', [ 'Solo', 'Platoon']],
'tank_tier' : [ 'Tank Tier', 'number' ], 'tank_tier' : [ 'Tank Tier', 'number' ],
'is_premium' : [ 'Tank Type', 'category', [ 'Tech tree', 'Premium']], 'is_premium' : [ 'Tank Type', 'category', [ 'Tech tree', 'Premium']],
'tank_type' : [ 'Tank Class', 'category', WG.TANK_TYPE_STR], 'tank_type' : [ 'Tank Class', 'category', WG.TANK_TYPE_STR],
@@ -321,8 +325,7 @@ class BattleCategorizationList():
def __init__(self, cats: list): def __init__(self, cats: list):
self.urls = collections.OrderedDict() self.urls = collections.OrderedDict()
self.url_title_max_len = 0 self.url_title_max_len = 0
cats = list(set(cats)) # remove duplicates cats = list(set(cats)) # remove duplicates
# ordering # ordering
@@ -352,6 +355,11 @@ class BattleCategorizationList():
bu.error('BattleCategorizationList()', exception=err) bu.error('BattleCategorizationList()', exception=err)
def __iter__(self):
"""Returns the Iterator object"""
return BattleCategorizationListIterator(self)
def get_categorization(self, cat: str): def get_categorization(self, cat: str):
"""Return a BattleCategorization for a category""" """Return a BattleCategorization for a category"""
try: try:
@@ -409,7 +417,7 @@ class BattleCategorizationList():
def get_filter_categories(self, cat: str, filters: list) -> dict: def get_filter_categories(self, cat: str, filters: list) -> dict:
try: try:
return self.categorizations[cat].get_filter_categories(filters) return self.categorizations[str(cat)].get_filter_categories(filters)
except Exception as err: except Exception as err:
bu.error(exception=err) bu.error(exception=err)
return None return None
@@ -455,6 +463,20 @@ class BattleCategorizationList():
return None return None
class BattleCategorizationListIterator():
def __init__(self, btl_cats: BattleCategorizationList) -> None:
self._btl_cats = btl_cats
self._ndx = 0
def __next__(self):
if self._ndx < len(self._btl_cats.categorizations_list):
cat = self._btl_cats.categorizations_list[self._ndx]
res = self._btl_cats.categorizations[cat]
self._ndx += 1
return res
raise StopIteration
class BattleCategorization(): class BattleCategorization():
RESULT_CAT_HEADER_LEN = 15 RESULT_CAT_HEADER_LEN = 15
@@ -489,10 +511,13 @@ class BattleCategorization():
def get_filter_categories(self, filters: list) -> list: def get_filter_categories(self, filters: list) -> list:
if type(filters) != list: try:
filters = [ filters ] if type(filters) != list:
return [ x for x in filters if x in self.get_categories() ] filters = [ filters ]
return [ x for x in filters if x in self.get_categories() ]
except Exception as err:
bu.error('Category: ' + str(self.category_key), exception=err)
return None
def help(self): def help(self):
"""Help""" """Help"""
@@ -711,7 +736,10 @@ class BattleStringCategorization(BattleCategorization):
def get_filter_categories(self, filters: list) -> list: def get_filter_categories(self, filters: list) -> list:
try: try:
return filters if type(filters) != list:
filters = [ filters ]
return [ x for x in filters if x in self.get_categories() ]
# return filters
except Exception as err: except Exception as err:
bu.error('Category: ' + str(self.category_key), exception=err) bu.error('Category: ' + str(self.category_key), exception=err)
return None return None
@@ -747,11 +775,14 @@ class BattleNumberCategorization(BattleCategorization):
def get_filter_categories(self, filters: list) -> list: def get_filter_categories(self, filters: list) -> list:
try: try:
if type(filters) != list:
filters = [ filters ]
return list(map(str,filters)) return list(map(str,filters))
except Exception as err: except Exception as err:
bu.error('Category: ' + str(self.category_key), exception=err) bu.error('Category: ' + str(self.category_key), exception=err)
return None return None
def help(self): def help(self):
super().help() super().help()
print('INTEGER') print('INTEGER')
@@ -892,15 +923,23 @@ class BattleCategory():
'battles%' : [ '% Battles', 'Share of Battles', 9, '{:9.0%}' ], 'battles%' : [ '% Battles', 'Share of Battles', 9, '{:9.0%}' ],
'win' : [ 'WR', 'Win rate', 6, '{:6.1%}' ], 'win' : [ 'WR', 'Win rate', 6, '{:6.1%}' ],
'damage_made' : [ 'DPB', 'Average Damage', 5, '{:5.0f}' ], 'damage_made' : [ 'DPB', 'Average Damage', 5, '{:5.0f}' ],
'damage_received' : [ 'DPBr', 'Average Damage Received', 5, '{:5.0f}' ],
'enemies_destroyed' : [ 'KPB', 'Kills / Battle', 4, '{:4.2f}' ], 'enemies_destroyed' : [ 'KPB', 'Kills / Battle', 4, '{:4.2f}' ],
'DR' : [ 'DR', 'Damage Ratio', 5, '{:5.1f}' ], 'DR' : [ 'DR', 'Damage Ratio', 5, '{:5.1f}' ],
'KDR' : [ 'KDR', 'Kills / Death', 4, '{:4.1f}' ], 'KDR' : [ 'KDR', 'Kills / Death', 4, '{:4.1f}' ],
'enemies_spotted' : [ 'Spot', 'Enemies spotted per battle', 4, '{:4.1f}' ], 'enemies_spotted' : [ 'Spot', 'Enemies spotted per battle', 4, '{:4.1f}' ],
'hit_rate' : [ 'Hit rate', 'Shots hit / all shots made', 8, '{:8.1%}' ], 'hit_rate' : [ 'Hit rate', 'Shots hit / all shots made', 8, '{:8.1%}' ],
'shots_made' : [ 'Shots', 'Shots made', 5, '{:5.1f}' ],
'hits_received' : [ 'Hits R.', 'Hits reveived', 7, '{:7.1f}' ],
'hits_bounced' : [ 'Bounced R.', 'Hits & bounced reveived', 10, '{:10.1f}' ],
'in_platoon' : [ 'Plat%', 'Platoon rate', 6, '{:6.1%}' ],
'pen_rate' : [ 'Pen rate', 'Shots pen / shots hit', 8, '{:8.1%}' ], 'pen_rate' : [ 'Pen rate', 'Shots pen / shots hit', 8, '{:8.1%}' ],
'pen_rate_received' : [ 'Penned%', 'Received hits pen / hits received', 7, '{:7.1%}' ],
'splash_rate' : [ 'Splash%', 'HE splash shots / shots hit', 7, '{:7.1%}' ],
'splash_rate_received' : [ 'Splashed%', 'HE splash shots received / hits hit', 9, '{:9.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', 'Time being alive in a battle in secs', 7, '{:7.0f}' ],
'alive' : [ 'Share live', '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}' ], 'battle_duration' : [ 'Duration', 'Battle duration', 8, '{:8.0f}' ],
'distance_travelled': [ 'Distance', 'Distance travelled', 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%}' ],
@@ -910,9 +949,9 @@ class BattleCategory():
'player_battles' : [ 'Player Btls', 'Average number battles of the player', 11, '{:11.0f}' ], 'player_battles' : [ 'Player Btls', 'Average number battles of the player', 11, '{:11.0f}' ],
'allies_battles' : [ 'Allies Btls', 'Average number battles of the allies', 11, '{:11.0f}' ], 'allies_battles' : [ 'Allies Btls', 'Average number battles of the allies', 11, '{:11.0f}' ],
'enemies_battles' : [ 'Enemies Btls', 'Average number battles of the enemies', 12, '{:12.0f}' ], 'enemies_battles' : [ 'Enemies Btls', 'Average number battles of the enemies', 12, '{:12.0f}' ],
'player_damage_dealt' : [ 'Player Avg Dmg', 'Player Average damage', 11, '{:11.0f}' ], 'player_damage_dealt' : [ 'Player Avg Dmg', 'Player Average damage', 14, '{:14.0f}' ],
'allies_damage_dealt' : [ 'Allies Avg Dmg', 'Average damage of the allies', 11, '{:11.0f}' ], 'allies_damage_dealt' : [ 'Allies Avg Dmg', 'Average damage of the allies', 14, '{:14.0f}' ],
'enemies_damage_dealt' : [ 'Enemies Avg Dmg', 'Average damage of the enemies', 12, '{:12.0f}' ], 'enemies_damage_dealt' : [ 'Enemies Avg Dmg', 'Average damage of the enemies', 15, '{:14.0f}' ],
MISSING_STATS : [ 'No stats', 'Players without stats avail', 8, '{:8.1%}'] MISSING_STATS : [ 'No stats', 'Players without stats avail', 8, '{:8.1%}']
} }
@@ -921,13 +960,15 @@ class BattleCategory():
_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', 'in_platoon', 'player_wins', 'allies_wins','enemies_wins'],
'team_extended' : [ 'battles', 'battles%', 'win','player_wins','allies_wins','enemies_wins', 'team_extended' : [ 'battles', 'battles%', 'win', 'in_platoon', 'player_wins', 'allies_wins', 'enemies_wins',
'player_damage_dealt', 'allies_damage_dealt', 'enemies_damage_dealt', 'player_damage_dealt', 'allies_damage_dealt', 'enemies_damage_dealt',
'player_battles','allies_battles','enemies_battles' ], 'player_battles','allies_battles','enemies_battles' ],
'extended' : [ 'battles', 'battles%', 'win','damage_made', 'enemies_destroyed', 'enemies_spotted', 'extended' : [ 'battles', 'battles%', 'win', 'in_platoon', 'damage_made', 'enemies_destroyed', 'enemies_spotted',
'top_tier', 'DR', 'KDR', 'hit_rate', 'pen_rate', 'survived', 'alive', 'top_tier', 'DR', 'KDR', 'hit_rate', 'pen_rate', 'survived', 'alive',
'player_wins', 'allies_wins', 'enemies_wins', MISSING_STATS ], 'player_wins', 'allies_wins', 'enemies_wins', MISSING_STATS ],
'dmg_received' : [ 'battles', 'win','damage_made', 'damage_received', 'enemies_destroyed', 'top_tier', 'DR',
'hits_received', 'hits_bounced', 'pen_rate_received', 'splash_rate_received'],
'all' : [ cat for cat in _result_fields.keys() ] 'all' : [ cat for cat in _result_fields.keys() ]
} }
@@ -940,11 +981,14 @@ class BattleCategory():
] ]
_result_ratios = { _result_ratios = {
'KDR' : [ 'enemies_destroyed', 'destroyed' ], 'KDR' : [ 'enemies_destroyed', 'destroyed' ],
'DR' : [ 'damage_made', 'damage_received' ], 'DR' : [ 'damage_made', 'damage_received' ],
'hit_rate' : [ 'shots_hit', 'shots_made' ], 'hit_rate' : [ 'shots_hit', 'shots_made' ],
'pen_rate' : [ 'shots_pen', 'shots_hit' ], 'splash_rate' : [ 'shots_splash', 'shots_hit' ],
'dmg_block_rate' : [ 'damage_blocked', 'damage_received' ] 'splash_rate_received' : [ 'hits_splash', 'hits_received' ],
'pen_rate' : [ 'shots_pen', 'shots_hit' ],
'pen_rate_received' : [ 'hits_pen', 'hits_received' ],
'dmg_block_rate' : [ 'damage_blocked', 'damage_received' ]
} }
# _extended_stats = False # _extended_stats = False
@@ -1014,12 +1058,21 @@ class BattleCategory():
@classmethod @classmethod
def get_field_name(cls, field: str): def get_field_name(cls, field: str) -> str:
try: try:
return cls._result_fields[field][0] return cls._result_fields[field][0]
except Exception as err: except Exception as err:
bu.error(exception=err) bu.error(exception=err)
@classmethod
def get_field_description(cls, field: str) -> str:
try:
return cls._result_fields[field][1]
except Exception as err:
bu.error(exception=err)
@classmethod @classmethod
def get_field_width(cls, field: str): def get_field_width(cls, field: str):
try: try:
@@ -1081,6 +1134,17 @@ class BattleCategory():
return None return None
@classmethod
def help(cls):
try:
for field in cls.get_result_fields_all():
print('{:24s} : {}'.format(field, cls.get_field_description(field)))
except Exception as err:
bu.error(exception=err)
return None
def __init__(self): def __init__(self):
try: try:
@@ -1419,9 +1483,10 @@ async def main(argv):
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('--json', action='store_true', default=False, help='Export data in JSON format')
parser.add_argument('--csv', action='store_true', default=False, help='Export data in CSV format') parser.add_argument('--csv', action='store_true', default=False, help='Export data in CSV format')
parser.add_argument('--replays', action='store_true', default=False, help='Export replay info in JSON 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: : [ {"tank_tier" : [8,9,10] }, { "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')
@@ -1498,7 +1563,7 @@ async def main(argv):
bu.debug('Waiting for the replay scanner to finish') bu.debug('Waiting for the replay scanner to finish')
await asyncio.wait([scanner_task]) await asyncio.wait([scanner_task])
bu.finish_progress_bar()
# bu.debug('Scanner finished. Waiting for replay readers to finish the queue') # bu.debug('Scanner finished. Waiting for replay readers to finish the queue')
await replayQ.join() await replayQ.join()
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
@@ -1508,15 +1573,21 @@ async def main(argv):
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
results = [] results = []
players = set() players = set()
replays = dict()
for res in await asyncio.gather(*reader_tasks): for res in await asyncio.gather(*reader_tasks):
results.extend(res[0]) results.extend(res[0])
players.update(res[1]) players.update(res[1])
replays.update(res[2])
if len(players) == 0: if len(players) == 0:
raise Exception("No players found to fetch stats for. No replays found?") raise Exception("No players found to fetch stats for. No replays found?")
if args.min != None: if args.min != None:
results = filter_min_replays_by_player(results, args.min) results = filter_min_replays_by_player(results, args.min)
players = get_players(results) players = get_players(results)
replays = filter_replays(replays, results)
(player_stats, stat_id_map) = await process_player_stats(players, OPT_WORKERS_N, args, db) (player_stats, stat_id_map) = await process_player_stats(players, OPT_WORKERS_N, args, db)
bu.debug('Number of player stats: ' + str(len(player_stats))) bu.debug('Number of player stats: ' + str(len(player_stats)))
@@ -1563,6 +1634,9 @@ async def main(argv):
csvwriter.writerows(rows) csvwriter.writerows(rows)
bu.verbose_std('Results exported to: ' + export_file) bu.verbose_std('Results exported to: ' + export_file)
if args.replays:
await export_replays(replays)
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)
@@ -1587,11 +1661,33 @@ async def help_extended(db : motor.motor_asyncio.AsyncIOMotorDatabase = None, pa
parser.print_help() parser.print_help()
# Result category options # Result category options
# Results fields
print('\n-------------------------------------------------------------------')
print('| Result fields |')
print('-------------------------------------------------------------------\n')
BattleCategory.help()
print('\n-------------------------------------------------------------------')
print('| Filters |')
print('-------------------------------------------------------------------\n')
print("\t--filters '{ \"categorization\" : category }'")
print('\tThese filters let you filter only battles matching the filter. The filter has been proper JSON dict.')
print("\tExample: --filter '{ \"room_type\" :1 }'")
print('')
BattleCategorizationList.help()
print('\n-------------------------------------------------------------------')
print('| Rounding errors |')
print('-------------------------------------------------------------------\n')
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.')
# Filter usage # Filter usage
print('\n-------------------------------------------------------------------') print('\n-------------------------------------------------------------------')
print('| DB Filter usage |') print('| DB Filter usage |')
print('-------------------------------------------------------------------') print('-------------------------------------------------------------------\n')
print('')
print('You are unlikely to have DB setup required for these filters. This is "Jylpah\'s special"') print('You are unlikely to have DB setup required for these filters. This is "Jylpah\'s special"')
print('') print('')
print("Syntax: --filters_db '{ \"replay.param\": VALUE, \"replay.param2\": { \"$MONGO_OPERATOR\" : VALUE }, ... }'") print("Syntax: --filters_db '{ \"replay.param\": VALUE, \"replay.param2\": { \"$MONGO_OPERATOR\" : VALUE }, ... }'")
@@ -1610,23 +1706,24 @@ async def help_extended(db : motor.motor_asyncio.AsyncIOMotorDatabase = None, pa
except Exception as err: except Exception as err:
bu.error(exception=err) bu.error(exception=err)
print('\n-------------------------------------------------------------------')
print('| Filters |') async def export_replays(replays: dict):
print('-------------------------------------------------------------------') """Export replays"""
print('') try:
print("\t--filters '{ \"categorization\" : category }'") replay_dir = OPT_EXPORT_REPLAY_DIR + '_' + bu.get_date_str()
print('\tThese filters let you filter only battles matching the filter. The filter has been proper JSON dict.') bu.verbose_std('Exporting replays to ' + replay_dir)
print("\tExample: --filter '{ \"room_type\" :1 }'") if not os.path.isdir(replay_dir):
print('') os.mkdir(replay_dir)
BattleCategorizationList.help()
print('\n-------------------------------------------------------------------') i = 0
print('| Rounding errors |') for replay in replays.values():
print('-------------------------------------------------------------------') filename = wg.get_replay_filename(replay)
print('') async with aiofiles.open(os.path.join(replay_dir, filename), 'w', encoding="utf8") as f:
print('Yes, the numbers are rounded UP because reasons.') await f.write(json.dumps(replay, indent=4))
print('If interested why please read this: ') i += 1
print('\thttps://realpython.com/python-rounding/#pythons-built-in-round-function') bu.verbose_std( str(i) + ' replays exported')
print('No, I am not planning to fix these for now.') except Exception as err:
bu.error(exception=err)
## move the class PlayerHistogram? ## move the class PlayerHistogram?
@@ -1668,7 +1765,7 @@ def filter_min_replays_by_player(results: list, min_replays: int) -> list:
for res in results: for res in results:
if res['protagonist'] in players_enough_replays: if res['protagonist'] in players_enough_replays:
res_ret.append(res) res_ret.append(res)
bu.verbose_std('Replays after filtering: ' + str(len(res_ret)))
return res_ret return res_ret
except Exception as err: except Exception as err:
@@ -1716,10 +1813,10 @@ def process_battle_results(results: dict, args : argparse.Namespace):
try: try:
url = args.url url = args.url
cats = BattleCategorizationList.get_categorizations(args) cats = BattleCategorizationList.get_categorizations(args)
blt_cat_list= None
if args.filters != None: if args.filters != None:
results = filter_replays(results, args.filters) results = filter_results(results, args.filters)
if len(results) > 0: if len(results) > 0:
blt_cat_list = BattleCategorizationList(cats) blt_cat_list = BattleCategorizationList(cats)
for result in results: for result in results:
@@ -1739,25 +1836,25 @@ def process_battle_results(results: dict, args : argparse.Namespace):
return blt_cat_list return blt_cat_list
def filter_replays(results: list, filter_json : str) -> bool: def filter_results(results: list, filter_json : str) -> bool:
"""Filter replays based on battle category filters""" """Filter replays based on battle category filters"""
try: try:
filters = json.loads(filter_json) filters = json.loads(filter_json)
bu.debug(str(filters)) bu.debug(str(filters))
for filter in filters:
if type(filters[filter]) != list:
filters[filter] = [ filters[filter]]
if type(filters) != dict: if type(filters) != dict:
bu.error('invalid --filter arguments: ' + str(filters)) bu.error('invalid --filter arguments: ' + str(filter))
sys.exit(1) sys.exit(1)
filter_cats = BattleCategorizationList(filters.keys()) filter_cats = BattleCategorizationList(filters.keys())
for cat in filters: bu.verbose('Filters applied:')
for category in filter_cats:
cat = category.category_key
categorization = filter_cats.get_categorization(cat) categorization = filter_cats.get_categorization(cat)
bu.debug('Category: ' + cat + ' filters type: ' + str(type(filters)) + ' filters: ' + str(filters))
filters[cat] = categorization.get_filter_categories(filters[cat]) filters[cat] = categorization.get_filter_categories(filters[cat])
bu.verbose('Filter: ' + cat + ': ' + ','.join(filters[cat])) bu.verbose(' ' + cat + ' = ' + ','.join(filters[cat]))
bu.verbose('')
res = list() res = list()
for result in results: for result in results:
try: try:
@@ -1773,13 +1870,27 @@ def filter_replays(results: list, filter_json : str) -> bool:
res.append(result) res.append(result)
except Exception as err: except Exception as err:
bu.error(exception=err) bu.error(exception=err)
bu.verbose('Replays after filtering: ' + str(len(res))) bu.verbose_std('Replays after filtering: ' + str(len(res)))
return res return res
except Exception as err: except Exception as err:
bu.error(exception=err) bu.error(exception=err)
return None return None
def filter_replays(replays_in: dict, results: list) -> list:
"""Filter source replays based on (filtered) results"""
replays = dict()
for res in results:
try:
# bu.debug('res[_id] : ' + res['_id'], force=True)
_id = res['_id']
replays[_id] = replays_in[_id]
except Exception as err:
bu.error(exception=err)
return replays
async def process_player_stats(players, N_workers: int, args : argparse.Namespace, db : motor.motor_asyncio.AsyncIOMotorDatabase) -> dict: async def process_player_stats(players, N_workers: int, args : argparse.Namespace, db : motor.motor_asyncio.AsyncIOMotorDatabase) -> dict:
"""Start stat_workers to retrieve and store players' stats""" """Start stat_workers to retrieve and store players' stats"""
## Create queue of player-tank pairs to find stats for ## Create queue of player-tank pairs to find stats for
@@ -2303,7 +2414,6 @@ async def mk_replayQ(queue : asyncio.Queue, args : argparse.Namespace, db : moto
Nreplays += 1 Nreplays += 1
except Exception as err: except Exception as err:
bu.error(exception=err) bu.error(exception=err)
bu.verbose('Finished scanning replays: ' + str(Nreplays) + ' replays to process')
return Nreplays return Nreplays
@@ -2314,8 +2424,9 @@ async def mk_readerQ_item(replay_json, filename : str = None, _id: str = None) -
try: try:
if wi.chk_JSON_replay(replay_json): 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) if _id != None:
replay_json['_id'] = _id _id = wi.read_replay_id(replay_json)
replay_json['_id'] = _id
else: else:
replay_json = None # mark error replay_json = None # mark error
except Exception as err: except Exception as err:
@@ -2334,6 +2445,7 @@ async def replay_reader(queue: asyncio.Queue, readerID: int, args : argparse.Nam
#global SKIPPED_N #global SKIPPED_N
results = [] results = []
playerstanks = set() playerstanks = set()
replays = dict()
try: try:
while True: while True:
item = await queue.get() item = await queue.get()
@@ -2360,8 +2472,10 @@ async def replay_reader(queue: asyncio.Queue, readerID: int, args : argparse.Nam
playerstanks.update(set(result['allies'])) playerstanks.update(set(result['allies']))
playerstanks.update(set(result['enemies'])) playerstanks.update(set(result['enemies']))
playerstanks.update(set([result['player']])) playerstanks.update(set([result['player']]))
_id = wi.read_replay_id(replay_json)
replays[_id] = replay_json
results.append(result) results.append(result)
bu.debug('Marking replay [' + str(replayID) + '] done', id=readerID) bu.debug('Marking replay [' + str(replayID) + '] done', id=readerID)
except Exception as err: except Exception as err:
@@ -2369,7 +2483,7 @@ async def replay_reader(queue: asyncio.Queue, readerID: int, args : argparse.Nam
queue.task_done() queue.task_done()
except (asyncio.CancelledError, concurrent.futures.CancelledError): except (asyncio.CancelledError, concurrent.futures.CancelledError):
bu.debug( str(len(results)) + ' replays, ' + str(len(playerstanks)) + ' player/tanks', readerID) bu.debug( str(len(results)) + ' replays, ' + str(len(playerstanks)) + ' player/tanks', readerID)
return results, playerstanks return results, playerstanks, replays
return None return None
@@ -2436,20 +2550,20 @@ async def read_replay_JSON(replay_json: dict, args : argparse.Namespace) -> dict
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)
if (protagonist != None) and (player['dbid'] == protagonist):
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 = dict()
tmp['account_id'] = account_id tmp['account_id'] = account_id
tmp['tank_id'] = player['vehicle_descr'] tank_id = player['vehicle_descr']
tmp['tank_id'] = tank_id
tmp['tank_tier'] = player_tank_tier tmp['tank_tier'] = player_tank_tier
tmp['tank_name'] = wg.get_tank_name(tmp['tank_id']) tmp['tank_name'] = wg.get_tank_name(tmp['tank_id'])
tmp['tank_type'] = wg.get_tank_type_id(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['tank_nation'] = wg.get_tank_nation_id(tmp['tank_id'])
tmp['is_premium'] = int(wg.is_premium(tmp['tank_id'])) tmp['is_premium'] = int(wg.is_premium(tmp['tank_id']))
tmp['squad_index'] = player['squad_index'] platoon_ndx = player['squad_index']
tmp['squad_index'] = platoon_ndx
tmp['in_platoon'] = 0 if platoon_ndx == None else 1
for key in replay_details_flds: for key in replay_details_flds:
tmp[key] = player[key] tmp[key] = player[key]
if player['hitpoints_left'] == 0: if player['hitpoints_left'] == 0:
@@ -2482,7 +2596,7 @@ async def read_replay_JSON(replay_json: dict, args : argparse.Namespace) -> dict
result['battle_duration'] = btl_duration 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(account_id, tank_id, 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:
+68 -22
View File
@@ -486,6 +486,16 @@ def NOW() -> int:
return int(time.time()) return int(time.time())
def get_date_str(timestamp:int = NOW(), date_format = '%Y%m%d_%H%M%S'):
"""Return YYYYMMDD_HHmm date string"""
try:
ts = datetime.fromtimestamp(timestamp)
return ts.strftime(date_format)
except Exception as err:
error(exception=err)
return None
def rebase_file_args(current_dir, files): def rebase_file_args(current_dir, files):
"""REbase file command line params after moving working dir to the script's dir""" """REbase file command line params after moving working dir to the script's dir"""
if isinstance(files, list): if isinstance(files, list):
@@ -633,6 +643,14 @@ 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]))
def sort_dict(d: dict, number: bool = False) -> dict:
"""Sort a dict by keys"""
if number:
return dict(sorted(d.items(), key=lambda item: int(item[0])))
else:
return dict(sorted(d.items(), key=lambda item: item[0]))
# ----------------------------------------------------------- # -----------------------------------------------------------
# Class SlowBar # Class SlowBar
# ----------------------------------------------------------- # -----------------------------------------------------------
@@ -916,33 +934,18 @@ class WG:
@classmethod @classmethod
def get_map(cls, map_str: str) -> str: def get_map(self, map_str: str) -> str:
"""Return map name from short map string in replays""" """Return map name from short map string in replays"""
try: try:
return cls.maps[map_str] return self.maps[map_str]
except: except:
error('Map ' + map_str + ' not found') error('Map ' + map_str + ' not found')
return None return None
@classmethod @classmethod
def get_map_user_strs(cls) -> str: def get_tank_user_strs(self) -> str:
return cls.maps.keys() return self.tanks["userStr"].keys()
@classmethod
def tank_str2name(cls, tank_str: str) -> str:
"""Return tank name from short tank string in replays"""
try:
return cls.tanks["userStr"][tank_str]
except:
error('Tank ' + tank_str + ' not found')
return tank_str
@classmethod
def get_tank_user_strs(cls) -> str:
return cls.tanks["userStr"].keys()
@classmethod @classmethod
@@ -1078,11 +1081,15 @@ class WG:
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:
return False return False
try: try:
with open(tankopedia_fn, 'rt', encoding='utf8') as f: with open(tankopedia_fn, 'rt', encoding='utf8') as f:
self.tanks = json.loads(f.read()) self.tanks = json.loads(f.read())
self.tanks['tankStr'] = dict()
p = re.compile('.+_short$')
for usr_str in self.tanks['userStr']:
if p.match(usr_str) == None:
self.tanks['tankStr'][self.tanks['userStr'][usr_str]] = usr_str
self.tanks_by_tier = dict() self.tanks_by_tier = dict()
for tier in range(1,11): for tier in range(1,11):
self.tanks_by_tier[str(tier)] = list() self.tanks_by_tier[str(tier)] = list()
@@ -1092,7 +1099,44 @@ class WG:
except Exception as err: except Exception as err:
error('Could not read tankopedia: ' + tankopedia_fn, err) error('Could not read tankopedia: ' + tankopedia_fn, err)
return False return False
def get_replay_filename(self, replay: dict):
try:
summary = replay['data']['summary']
timestamp = int(summary['battle_start_timestamp'])
timestamp = get_date_str(timestamp, date_format='%Y%m%d_%H%M')
player = summary['player_name']
arena_id = int(summary['arena_unique_id'])
vehicle = self.name2tank_str(summary['vehicle'])
vehicle.replace(' ','_')
return '{:s}_{:s}_{:s}_{:d}.wotbreplay.json'.format(timestamp, player, vehicle, arena_id)
except Exception as err:
error('Unexpected Exception', err)
return None
def get_map_user_strs(self) -> str:
return self.maps.keys()
def tank_str2name(self, tank_str: str) -> str:
"""Return tank name from short tank string in replays"""
try:
return self.tanks["userStr"][tank_str]
except:
error('Tank ' + tank_str + ' not found')
return tank_str
def name2tank_str(self, name: str) -> str:
"""Return shorttank string from tank string in replays"""
try:
return self.tanks["tankStr"][name]
except:
debug('Tank ' + name + ' not found')
return name
def get_tanks_by_tier(self, tier: int) -> list(): def get_tanks_by_tier(self, tier: int) -> list():
"""Returns tank_ids by tier""" """Returns tank_ids by tier"""
@@ -1831,6 +1875,8 @@ class WoTinspector:
except Exception as err: except Exception as err:
debug(exception=err, id=N) debug(exception=err, id=N)
await asyncio.sleep(SLEEP) await asyncio.sleep(SLEEP)
_id = self.read_replay_id(json_resp)
json_resp['_id'] = _id
debug(' Could not post replay: ' + title, id=N) debug(' Could not post replay: ' + title, id=N)
return json_resp return json_resp
+2 -2
View File
@@ -79,8 +79,8 @@ async def main(argv):
tankopedia = collections.OrderedDict() tankopedia = collections.OrderedDict()
tankopedia['status'] = 'ok' tankopedia['status'] = 'ok'
tankopedia['meta'] = { "count": len(tanks) } tankopedia['meta'] = { "count": len(tanks) }
tankopedia['data'] = tanks tankopedia['data'] = bu.sort_dict(tanks, number=True)
tankopedia['userStr'] = userStrs tankopedia['userStr'] = bu.sort_dict(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') 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))
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/local/bin/python3.7 #!/usr/bin/env python3
# Get Tankpedia from WoTinspector.com # Get Tankpedia from WoTinspector.com
+1
View File
@@ -43,6 +43,7 @@
"neptune_02": "Normandy - Beach", "neptune_02": "Normandy - Beach",
"north": "North", "north": "North",
"ordeal": "Trial by Fire", "ordeal": "Trial by Fire",
"plant": "Ghost Factory",
"pliego": "Castilla", "pliego": "Castilla",
"port": "Port Bay", "port": "Port Bay",
"rift": "Hellas", "rift": "Hellas",
+5
View File
@@ -2,6 +2,11 @@
| Release | Date | Notes | Inaccurate? | | Release | Date | Notes | Inaccurate? |
|:-------:|:--------:|-------------|-----| |:-------:|:--------:|-------------|-----|
| 8.4 | 2021-10-20 | Polish heavy tanks | |
| 8.3 | 2021-09-16 | FV4005, FV4022, VK 91.01 (P) and WZ-121 legendary camos, -5 deg gun depression to WZ-120 | |
| 8.2 | 2021-08-18 | Ghost Factory map, XP/credits nerf for inactive players, new store UI | |
| 8.1 | 2021-07-15 | Armor highlighting updated | |
| 8.0 | 2021-06-08 | American experimental heavies (Yoh-line), PBR camos | |
| 7.9 | 2021-04-27 | Mines rework, new tutorial, colorblind mode, JgPz E100 legendary camo | | | 7.9 | 2021-04-27 | Mines rework, new tutorial, colorblind mode, JgPz E100 legendary camo | |
| 7.8 | 2021-03-24 | Mid-tier balance changes, Grille line new consumables | | | 7.8 | 2021-03-24 | Mid-tier balance changes, Grille line new consumables | |
| 7.7 | 2021-02-24 | Czechoslovakian medium tanks | | | 7.7 | 2021-02-24 | Czechoslovakian medium tanks | |
+460 -97
View File
File diff suppressed because it is too large. Load diff
+8 -8
View File
@@ -195,7 +195,7 @@ async def replayWorker(queue: asyncio.Queue, workerID: int, account_id: int, pri
bu.verbose_std(msg_str + title + ' has already been posted. Skipping.' ) bu.verbose_std(msg_str + title + ' has already been posted. Skipping.' )
else: else:
os.remove(replay_json_fn) os.remove(replay_json_fn)
bu.debug(msg_str + "Replay JSON not valid: Deleting " + replay_json_fn, id=workerID) bu.debug(msg_str + "Replay JSON not valid/complete: Deleting " + replay_json_fn, id=workerID)
SKIPPED_N += 1 SKIPPED_N += 1
queue.task_done() queue.task_done()
continue continue
@@ -216,13 +216,13 @@ async def replayWorker(queue: asyncio.Queue, workerID: int, account_id: int, pri
if not bu.debug(msg_str + 'Replay saved OK: ' + filename): if not bu.debug(msg_str + 'Replay saved OK: ' + filename):
bu.verbose_std(msg_str + title + ' posted') bu.verbose_std(msg_str + title + ' posted')
else: else:
bu.warning(msg_str +'Replay file is not valid: ' + filename) bu.warning(msg_str +'Replay file is not valid/complete: ' + filename)
ERROR_N += 1 ERROR_N += 1
else: else:
bu.error(msg_str + 'Error saving replay: ' + filename) bu.error(msg_str + 'Error saving replay: ' + filename)
ERROR_N += 1 ERROR_N += 1
else: else:
bu.error(msg_str + 'Replay file is not valid: ' + filename) bu.error(msg_str + 'Replay file is not valid/complete: ' + filename)
ERROR_N += 1 ERROR_N += 1
except Exception as err: except Exception as err:
bu.error(msg_str + 'Unexpected Exception: ' + str(type(err)) + ' : ' + str(err) ) bu.error(msg_str + 'Unexpected Exception: ' + str(type(err)) + ' : ' + str(err) )
@@ -286,14 +286,14 @@ def getTitle(replayfile: str, title: str = None, i : int = 0) -> str:
# bu.debug('Replay file\'s metadata: opened') # bu.debug('Replay file\'s metadata: opened')
try: try:
metadata_json = json.load(meta) metadata_json = json.load(meta)
#player = metadata_json['playerName'] #player = metadata_json['playerName']
tank = wg.tank_str2name(metadata_json['playerVehicleName']) tank_name = wg.get_tank_name(metadata_json['vehicleCompDescriptor'])
map_name = wg.get_map(metadata_json['mapName']) map_name = wg.get_map(metadata_json['mapName'])
bu.debug('Tank: ' + tank + ' Map: ' + map_name) bu.debug('Tank: ' + tank_name if tank_name != None else "NOT FOUND IN tanks.json" + ' Map: ' + map_name)
except Exception as err: except Exception as err:
bu.error(exception = err) bu.error(exception = err)
if (tank != None) and (map_name != None): if (tank_name != None) and (map_name != None):
title = tank + ' @ ' + map_name title = tank_name + ' @ ' + map_name
else: else:
title = re.sub('\\.wotbreplay$', '', filename) title = re.sub('\\.wotbreplay$', '', filename)
except Exception as err: except Exception as err: