Files
blitz-tools/analyze_wotb_replays.py
T

3368 lines
112 KiB
Python
Raw Normal View History

2021-01-25 09:26:57 +02:00
#!/usr/bin/env python3
## PYTHON VERSION MUST BE 3.7 OR HIGHER
2018-10-08 00:14:56 +08:00
# Script Analyze WoT Blitz replays
import sys, argparse, json, os, concurrent, inspect, aiohttp, asyncio, aiofiles, aioconsole
2023-09-10 20:00:23 +03:00
import configparser, ssl, re, logging, time, xmltodict, collections, csv
from datetime import datetime, timedelta, date
2023-09-10 20:00:23 +03:00
from motor.motor_asyncio import AsyncIOMotorDatabase, AsyncIOMotorClient
import blitzutils1 as bu
from blitzutils1 import WG
from blitzutils1 import WoTinspector
2018-10-08 00:14:56 +08:00
logging.getLogger("asyncio").setLevel(logging.DEBUG)
2023-09-10 20:00:23 +03:00
WG_APP_ID = "df25c506c7944e48fbec67e8b74fd34e"
FILE_CONFIG = "blitzstats.ini"
DB_C_TANK_STATS = "WG_TankStats"
DB_C_PLAYER_STATS = "WG_PlayerStats"
DB_C_REPLAYS = "Replays"
2023-09-10 20:00:23 +03:00
N_PLAYERS = "Nplayers"
MISSING_STATS = "missing_stats"
2020-02-13 00:21:36 +02:00
2023-09-10 20:00:23 +03:00
wg: WG
wi: WoTinspector
2018-10-08 00:14:56 +08:00
REPLAY_N = 0
2019-08-17 00:50:48 +03:00
REPLAY_I = 0
2018-10-08 00:14:56 +08:00
STAT_TANK_BATTLE_MIN = 100
2023-09-10 20:00:23 +03:00
BATTLE_TIME_BUCKET = 3600 * 24 * 14
2018-10-08 00:14:56 +08:00
2023-09-10 20:00:23 +03:00
OPT_EXPORT_REPLAY_DIR = "export_replays"
OPT_EXPORT_CSV_FILE = "export.csv"
OPT_EXPORT_JSON_FILE = "export.json"
RE_SRC_IS_DB = re.compile(r"^DB:")
2020-07-10 14:32:12 +03:00
2021-01-01 12:22:07 +02:00
class StatFunc:
2023-09-10 20:00:23 +03:00
## For different player stat functions (global stats, tank tier stats, etc)
# 1st function = for forming stat_id, 2nd for DB stats query, 3rd for WG API stats query
STAT_FUNC = {
"tank_tier": [
"get_stat_id_tank_tier",
"get_db_tank_tier_stats",
"get_wg_tank_tier_stats",
"WR at tier",
],
"tank": [
"get_stat_id_tank",
"get_db_tank_stats",
"get_wg_tank_stats",
"WR in Tank",
],
"player": [
"get_stat_id_player",
"get_db_player_stats",
"get_wg_player_stats",
"Career WR",
],
"tier_x": [
"get_stat_id_player",
"get_db_tier_x_stats",
"get_wg_tier_x_stats",
"WR at tier X",
],
}
2023-09-10 20:00:23 +03:00
_DEFAULT = "player"
2021-01-01 12:22:07 +02:00
2023-09-10 20:00:23 +03:00
_stat_func = _DEFAULT
2023-09-10 20:00:23 +03:00
@classmethod
def get_default(cls):
return cls._DEFAULT
2023-09-10 20:00:23 +03:00
@classmethod
def get_stat_funcs(cls):
return cls.STAT_FUNC.keys()
2023-09-10 20:00:23 +03:00
@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
2023-09-10 20:00:23 +03:00
@classmethod
def get_stat_func(cls):
return cls._stat_func
2021-01-01 12:22:07 +02:00
2023-09-10 20:00:23 +03:00
@classmethod
def get_stat_id_func(cls):
return cls.STAT_FUNC[cls._stat_func][0]
2021-01-01 12:22:07 +02:00
2023-09-10 20:00:23 +03:00
@classmethod
def get_db_func(cls):
return cls.STAT_FUNC[cls._stat_func][1]
2021-01-01 12:22:07 +02:00
2023-09-10 20:00:23 +03:00
@classmethod
def get_wg_func(cls):
return cls.STAT_FUNC[cls._stat_func][2]
2021-01-01 12:22:07 +02:00
2023-09-10 20:00:23 +03:00
@classmethod
def get_title(cls):
return cls.STAT_FUNC[cls._stat_func][3]
2021-01-01 12:22:07 +02:00
2018-10-08 00:14:56 +08:00
replay_summary_flds = [
2023-09-10 20:00:23 +03:00
"battle_result",
"battle_type",
"map_name",
"battle_duration",
"room_type",
"protagonist",
"player_name",
"title",
"mastery_badge",
2018-10-08 00:14:56 +08:00
]
replay_details_flds = [
2023-09-10 20:00:23 +03:00
"damage_made",
"damage_assisted_track",
"damage_assisted",
"damage_received",
"damage_blocked",
"enemies_damaged",
"enemies_destroyed",
"hitpoints_left",
"time_alive",
"distance_travelled",
"base_capture_points",
"base_defend_points",
"wp_points_earned",
"wp_points_stolen",
"hits_received",
"hits_bounced",
"hits_splash",
"hits_pen",
"shots_made",
"shots_hit",
"shots_pen",
"shots_splash",
"enemies_spotted",
]
2018-10-08 00:14:56 +08:00
## Syntax: key == stat field in https://api.wotblitz.eu/wotb/tanks/stats/ (all.KEY)
## Value array [ 'Stat Title', [ 0, data buckets ....], scaling_factor_for_bucket_values, 'print_format' ]
histogram_fields = {
2023-09-10 20:00:23 +03:00
"wins": [
"Win rate",
[0, 0.35, 0.40, 0.45, 0.48, 0.5, 0.52, 0.55, 0.60, 0.65, 0.70, 1],
1,
"{:2.0%}",
],
"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],
0.001,
"{:.0f}k",
], # battles is a mandatory stat to include
}
2018-10-08 00:14:56 +08:00
def def_value_zero():
2018-10-08 00:14:56 +08:00
return 0
2023-09-10 20:00:23 +03:00
def def_value_BattleCategory():
2023-09-10 20:00:23 +03:00
return BattleCategory()
class BattleCategorizationList:
@staticmethod
def mk_battle_modes(modes: dict):
"""Make a list of battle modes as required for the _categorizations"""
mode_ids = modes.values()
id_max = max(mode_ids)
ret_modes = ["-"] * (id_max + 16)
for i in range(0, id_max + 16):
ret_modes[i] = "room_type=" + str(i)
for mode in modes:
id = modes[mode]
ret_modes[id] = mode
return ret_modes
_battle_modes = {
"Any": 0,
"Regular": 1,
"Training Room": 2,
"Tournament": 4,
"Quick Tournament": 5, # maybe?
"Rating": 7,
"Mad Games": 8,
"Realistic Battles": 22,
"Uprising": 23,
"Gravity Mode": 24,
"Skirmish": 25,
"Burning Games": 26,
}
_BATTLE_MODES = mk_battle_modes.__func__(_battle_modes)
_categorizations = {
"total": ["TOTAL", "total"],
"battle_result": ["Result", "category", ["Loss", "Win", "Draw"]],
"battle_type": ["Battle Type", "category", ["Encounter", "Supremacy"]],
"room_type": ["Battle Mode", "category", _BATTLE_MODES],
"top_tier": ["Tier", "category", ["Bottom tier", "Top tier"]],
"in_platoon": ["Platoon", "category", ["Solo", "Platoon"]],
"tank_tier": ["Tank Tier", "number"],
"is_premium": ["Tank Type", "category", ["Tech tree", "Premium"]],
"tank_type": ["Tank Class", "category", WG.TANK_TYPE_STR],
"tank_nation": ["Nation", "category", WG.NATION_STR],
"mastery_badge": [
"Battle Medal",
"category",
["-", "3rd Class", "2nd Class", "1st Class", "Mastery"],
],
"team_result": ["Team Result", "string"],
"player_wins": ["Player WR", "bucket", [0, 0.35, 0.45, 0.50, 0.55, 0.65], "%"],
"player_battles": [
"Player Battles",
"bucket",
[0, 500, 1000, 2500, 5e3, 10e3, 15e3, 25e3],
"int",
],
"player_damage_dealt": [
"Player Avg Dmg",
"bucket",
[0, 500, 1000, 1250, 1500, 1750, 2000, 2500],
"int",
],
"damage_made": [
"Player Dmg made",
"bucket",
[0, 500, 1000, 1250, 1500, 1750, 2000, 2500],
"int",
],
"enemies_destroyed": ["Player Kills", "number"],
"enemies_spotted": ["Player Spots", "number"],
"hit_rate": [
"Player Hit Rate",
"bucket",
[0, 0.25, 0.5, 0.6, 0.7, 0.8, 0.9, 0.95],
"%",
],
"pen_rate": [
"Player Pen Rate",
"bucket",
[0, 0.25, 0.5, 0.6, 0.7, 0.8, 0.9, 0.95],
"%",
],
"alive": [
"Time alive (pcs)",
"bucket",
[x / 100 for x in range(0, 100, 10)],
"%",
],
"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, 0.35, 0.45, 0.50, 0.55, 0.65], "%"],
"allies_battles": [
"Player Battles",
"bucket",
[0, 500, 1000, 2500, 5e3, 10e3, 15e3, 25e3],
"int",
],
"allies_damage_dealt": [
"Player Avg Dmg",
"bucket",
[0, 500, 1000, 1250, 1500, 1750, 2000, 2500],
"int",
],
"enemies_wins": [
"Enemies WR",
"bucket",
[0, 0.35, 0.45, 0.50, 0.55, 0.65],
"%",
],
"enemies_battles": [
"Player Battles",
"bucket",
[0, 500, 1000, 2500, 5e3, 10e3, 15e3, 25e3],
"int",
],
"enemies_damage_dealt": [
"Player Avg Dmg",
"bucket",
[0, 500, 1000, 1250, 1500, 1750, 2000, 2500],
"int",
],
"player_name": ["Player", "string", 25],
"protagonist": ["account_id", "number"],
"tank_name": ["Tank", "string", 25],
"map_name": ["Map", "string", 20],
"battle": ["Battle", "string", 40],
"battle_i": ["Battle #", "number"],
}
_categorizations_default = [
"total",
"battle_result",
"battle_type",
"tank_tier",
"top_tier",
]
## Player stats based categorizations
_categorizations_stats = [
group + "_" + measure
for group in ["player", "allies", "enemies"]
for measure in ["wins", "battles", "damage_dealt"]
]
@classmethod
def get_categorizations_all(cls, cats: list = None) -> list:
try:
if cats is None:
return list(cls._categorizations.keys())
else:
return list(set(cls._categorizations.keys()) & set(cats))
except Exception as err:
bu.error(exception=err)
return None
@classmethod
def get_categorizations_stats(cls, cats=None, stats_cats=None):
try:
if cats is None:
return cls._categorizations_stats
else:
if stats_cats is None:
return cats
player_stat_filters = set(cls._categorizations_stats)
cats = set(cls.get_categorizations_all(cats))
if stats_cats:
cats = cats & player_stat_filters
else:
cats = cats - player_stat_filters
bu.debug(", ".join(list(cats)))
return list(cats)
except Exception as err:
bu.error(exception=err)
return None
@classmethod
def get_categorizations_default(cls):
return cls._categorizations_default
@classmethod
def set_categorizations_default(cls, cat_defaults: list):
try:
defaults = list()
for cat in cat_defaults:
if cat in cls.get_categorizations_all():
defaults.append(cat)
else:
bu.error(cat + " is not a defined categorization")
cls._categorizations_default = defaults
except Exception as err:
bu.error(exception=err)
@classmethod
def get_categorizations_extra(cls) -> list:
# return cls._categorizations_extra
return list(
set(cls._categorizations.keys()) - set(cls.get_categorizations_default())
)
@classmethod
def get_categorization_title(cls, cat: str) -> str:
try:
return cls._categorizations[cat][0]
except Exception as err:
bu.error(exception=err)
return None
@classmethod
def get_categorization_type(cls, cat: str) -> str:
try:
return cls._categorizations[cat][1]
except Exception as err:
bu.error(exception=err)
return None
@classmethod
def get_categorization_params(cls, cat: str) -> list:
try:
if len(cls._categorizations[cat]) > 2:
return cls._categorizations[cat][2:]
except Exception as err:
bu.error(exception=err)
return None
@classmethod
def help(cls):
for cat in cls.get_categorizations_all():
try:
ctitle = cls.get_categorization_title(cat)
ctype = cls.get_categorization_type(cat)
cparams = cls.get_categorization_params(cat)
if ctype == "number":
cat_tmp = BattleNumberCategorization(cat, ctitle)
elif ctype == "string":
cat_tmp = BattleStringCategorization(cat, ctitle, cparams)
elif ctype == "category":
cat_tmp = BattleClassCategorization(cat, ctitle, cparams[0])
elif ctype == "bucket":
cat_tmp = BattleBucketCategorization(
cat, ctitle, cparams[0], cparams[1]
)
else:
cat_tmp = BattleTotals(ctitle)
cat_tmp.help()
except KeyError as err:
bu.error(
"BattleCategorizationList(): Key not found: Category: " + cat,
exception=err,
)
except Exception as err:
bu.error("BattleCategorizationList()", exception=err)
@classmethod
def get_categorizations(cls, args: argparse.Namespace):
extra_cats = args.extra
only_extra = args.only_extra
if only_extra:
cats = list()
else:
cats = cls.get_categorizations_default()
if extra_cats is not None:
cats = cats + extra_cats
return cats
def __init__(self, cats: list):
self.urls = collections.OrderedDict()
self.url_title_max_len = 0
cats = list(set(cats)) # remove duplicates
# ordering
self.categorizations_list = [
cat for cat in self._categorizations if cat in cats
]
self.categorizations = dict()
for cat in self.categorizations_list:
try:
ctitle = self.get_categorization_title(cat)
ctype = self.get_categorization_type(cat)
cparams = self.get_categorization_params(cat)
if ctype == "total":
self.categorizations[cat] = BattleTotals(ctitle)
elif ctype == "number":
self.categorizations[cat] = BattleNumberCategorization(cat, ctitle)
elif ctype == "string":
self.categorizations[cat] = BattleStringCategorization(
cat, ctitle, cparams
)
elif ctype == "category":
self.categorizations[cat] = BattleClassCategorization(
cat, ctitle, cparams[0]
)
elif ctype == "bucket":
self.categorizations[cat] = BattleBucketCategorization(
cat, ctitle, cparams[0], cparams[1]
)
except KeyError as err:
bu.error(
"BattleCategorizationList(): Key not found: Category: " + cat,
exception=err,
)
except Exception as err:
bu.error("BattleCategorizationList()", exception=err)
def __iter__(self):
"""Returns the Iterator object"""
return BattleCategorizationListIterator(self)
def len(self):
return len(self.categorizations)
def get_categorization(self, cat: str):
"""Return a BattleCategorization for a category"""
try:
return self.categorizations[cat]
except Exception as err:
bu.error(exception=err)
return None
def record_result(self, result: dict):
try:
for cat in self.categorizations_list:
self.categorizations[cat].record_result(result)
except Exception as err:
bu.error(exception=err)
return None
def calc_results(self):
try:
for cat in self.categorizations_list:
self.categorizations[cat].calc_results()
except Exception as err:
bu.error(exception=err)
return None
def record_url(self, result):
try:
self.url_title_max_len = max(self.url_title_max_len, len(result["title"]))
self.urls[result["title"]] = result["url"]
except Exception as err:
bu.error(exception=err)
return None
def print_results(self):
try:
for cat in self.categorizations_list:
self.categorizations[cat].print_results()
self.print_urls()
except Exception as err:
bu.error(exception=err)
def print_urls(self):
try:
if len(self.urls) > 0:
print("\nURLs to Battle replays:")
for title in self.urls:
print(
("{:" + str(3 + self.url_title_max_len) + "}").format(title)
+ " : "
+ self.urls[title]
)
except Exception as err:
bu.error(exception=err)
def get_filter_categories(self, cat: str, filters: list) -> dict:
try:
return self.categorizations[str(cat)].get_filter_categories(filters)
except Exception as err:
bu.error(exception=err)
return None
def get_categories(self, result: dict) -> dict:
"""Get categories for each categorization in the CategorizationList() for a battle result"""
try:
res = dict()
for cat in self.categorizations_list:
res[cat] = self.categorizations[cat].get_category(result)
return res
except Exception as err:
bu.error(exception=err)
return None
def get_results_json(self):
try:
res = dict()
for cat in self.categorizations_list:
res[cat] = self.categorizations[cat].get_results_json()
res["replay_urls"] = self.urls
return res
except Exception as err:
bu.error(exception=err)
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 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:
RESULT_CAT_HEADER_LEN = 15
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):
self.total_battles = 0
self.category_key = cat_key
self.title = title
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):
"""Get category keys sorted"""
return sorted(self.categories.keys(), key=str.casefold)
### PER Child Class
def get_category(self, result: dict) -> str:
"""Get category. Must be implemented in the child classes"""
return "NONE"
def get_filter_categories(self, filters: list) -> list:
try:
if type(filters) != list:
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):
"""Help"""
FORMAT = "\t{:<15s}\t{}. Options:"
print(FORMAT.format(self.category_key, self.title), end=" ")
def record_result(self, result: dict) -> bool:
try:
cat = self.get_category(result)
self.categories[cat].record_result(result)
self.total_battles += 1
return True
except KeyError as err:
bu.error("Category key not found: " + cat, exception=err)
except Exception as err:
bu.error("Category key: " + cat, exception=err)
return False
def calc_results(self):
try:
for cat in self.get_categories():
self.categories[cat].calc_results(self.total_battles)
except Exception as 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):
try:
bu.print_new_line()
self.print_headers()
for cat in self.get_categories():
if len(cat) > self.RESULT_CAT_HEADER_LEN:
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()
except KeyError as err:
bu.error("Key not found", err)
except Exception as err:
bu.error(exception=err)
return None
def get_results_json(self) -> dict:
"""Get results in JSON format"""
try:
results = dict()
results["categorization"] = self.title
results["category_key"] = self.category_key
for cat in self.get_categories():
results[cat] = self.categories[cat].get_results()
return results
except KeyError as err:
bu.error("Key not found", err)
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
2021-02-05 21:41:50 +02:00
2020-10-31 14:26:57 +02:00
class BattleTotals(BattleCategorization):
2023-09-10 20:00:23 +03:00
TOTAL = "Total"
2020-11-01 01:53:26 +02:00
2023-09-10 20:00:23 +03:00
def __init__(self, title: str):
super().__init__(self.TOTAL, title)
2020-11-01 01:53:26 +02:00
2023-09-10 20:00:23 +03:00
def get_category(self, result: dict) -> str:
return self.TOTAL
2020-11-01 01:53:26 +02:00
2023-09-10 20:00:23 +03:00
def get_categories(self) -> str:
return [self.TOTAL]
2020-11-01 01:53:26 +02:00
2023-09-10 20:00:23 +03:00
def help(self):
super().help()
print("-")
2020-11-24 22:49:03 +02:00
2020-10-31 14:26:57 +02:00
class BattleClassCategorization(BattleCategorization):
2023-09-10 20:00:23 +03:00
"""Class for categorization by fixed classes with ID (int)"""
2020-10-31 14:26:57 +02:00
2023-09-10 20:00:23 +03:00
def __init__(self, cat_key: str, title: str, classes: list):
super().__init__(cat_key, title)
self.category_labels = classes
2020-10-31 14:26:57 +02:00
2023-09-10 20:00:23 +03:00
def help(self):
super().help()
labels = list()
for ndx in range(0, len(self.category_labels)):
labels.append(str(ndx) + "=" + self.category_labels[ndx])
print(", ".join(labels))
2020-11-24 22:49:03 +02:00
2023-09-10 20:00:23 +03:00
def get_category(self, result: dict) -> str:
"""Get category"""
cat_id = -1
try:
cat_id = result[self.category_key]
return self.category_labels[cat_id]
except KeyError as err:
bu.error(
"Category: " + str(self.category_key) + " ID: " + str(cat_id),
exception=err,
)
except Exception as err:
bu.error(
"Category: " + str(self.category_key) + " ID: " + str(cat_id),
exception=err,
)
return None
2020-11-24 22:49:03 +02:00
2023-09-10 20:00:23 +03:00
def get_filter_categories(self, filters: list) -> list:
try:
# bu.debug(str(filters), force=True)
if type(filters) != list:
filters = [filters]
bu.debug(str(self.category_labels))
res = list()
for ndx in filters:
if ndx in range(0, len(self.category_labels)):
res.append(self.category_labels[ndx])
return res
# return [ self.category_labels[int(x)] for x in filters if x in range(0, len(self.get_categories())) ]
except Exception as err:
bu.error("Category: " + str(self.category_key), exception=err)
return None
2020-11-01 01:53:26 +02:00
2023-09-10 20:00:23 +03:00
def get_categories(self) -> list:
try:
cats = list()
for cat in self.category_labels:
if cat in self.categories.keys():
cats.append(cat)
return cats
except KeyError as err:
bu.error("Key not found", exception=err)
except Exception as err:
bu.error(exception=err)
return None
2020-11-01 01:53:26 +02:00
class BattleStringCategorization(BattleCategorization):
2023-09-10 20:00:23 +03:00
"""Class for categorization by string"""
2020-11-01 01:53:26 +02:00
2023-09-10 20:00:23 +03:00
def __init__(self, cat_key: str, title: str, params: list):
super().__init__(cat_key, title)
if params is not 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]
2023-09-10 20:00:23 +03:00
self.set_cat_header_format(width, left_align)
2023-09-10 20:00:23 +03:00
def get_category(self, result: dict) -> str:
"""Get category"""
try:
return result[self.category_key]
except KeyError as err:
bu.error("Category key: " + self.category_key + " not found", exception=err)
except Exception as err:
bu.error("Category: " + self.category_key, exception=err)
return None
2020-10-31 14:26:57 +02:00
2023-09-10 20:00:23 +03:00
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]) is not 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)
2023-09-10 20:00:23 +03:00
except Exception as err:
bu.error("Category: " + self.category_key, exception=err)
return None
2020-11-25 23:24:31 +02:00
2023-09-10 20:00:23 +03:00
def get_filter_categories(self, filters: list) -> list:
try:
if type(filters) != list:
filters = [filters]
return [x for x in filters if x in self.get_categories()]
# return filters
except Exception as err:
bu.error("Category: " + str(self.category_key), exception=err)
return None
2020-11-25 23:24:31 +02:00
2023-09-10 20:00:23 +03:00
def help(self):
super().help()
print(self.title + " name")
2020-11-24 22:49:03 +02:00
2020-10-31 14:26:57 +02:00
class BattleNumberCategorization(BattleCategorization):
2023-09-10 20:00:23 +03:00
"""Class for categorization by number"""
2020-11-01 01:53:26 +02:00
2023-09-10 20:00:23 +03:00
def get_category(self, result: dict) -> str:
"""Get category as"""
try:
return str(result[self.category_key])
except KeyError as err:
bu.error("Category: " + str(self.category_key), exception=err)
2020-11-01 01:53:26 +02:00
2023-09-10 20:00:23 +03:00
except Exception as err:
bu.error("Category: " + str(self.category_key), exception=err)
return None
2020-11-01 01:53:26 +02:00
2023-09-10 20:00:23 +03:00
def get_categories(self) -> list:
try:
return map(str, sorted(map(int, self.categories.keys())))
except KeyError as err:
bu.error("Key not found", exception=err)
return None
2020-10-31 14:26:57 +02:00
2023-09-10 20:00:23 +03:00
def get_filter_categories(self, filters: list) -> list:
try:
if type(filters) != list:
filters = [filters]
return list(map(str, filters))
except Exception as err:
bu.error("Category: " + str(self.category_key), exception=err)
return None
2020-10-31 14:26:57 +02:00
2023-09-10 20:00:23 +03:00
def help(self):
super().help()
print("INTEGER")
2020-11-24 22:49:03 +02:00
2020-10-31 14:26:57 +02:00
class BattleBucketCategorization(BattleCategorization):
2023-09-10 20:00:23 +03:00
"""Class for categorization by buckets based on value"""
def __init__(self, cat_key: str, title: str, breaks: list, bucket_format=""):
try:
super().__init__(cat_key, title)
self.breaks = breaks
self.bucket_format = bucket_format
self.category_labels = self.mk_category_labels(breaks, bucket_format)
except Exception as err:
bu.error("BattleBucketCategorization()", exception=err)
def help(self):
super().help()
labels = list()
for ndx in range(0, len(self.category_labels)):
labels.append(str(ndx) + "=" + self.category_labels[ndx])
print(", ".join(labels))
def get_category(self, result: dict) -> str:
"""Get category"""
cat_id = -1
value = None
try:
value = result[self.category_key]
cat_id = self.find_bucket(value, self.breaks)
return self.category_labels[cat_id]
except KeyError as err:
bu.error(
"Category: "
+ str(self.category_key)
+ " value: "
+ str(value)
+ " cat ID: "
+ str(cat_id),
exception=err,
)
except Exception as err:
bu.error(
"Category: "
+ str(self.category_key)
+ " value: "
+ str(value)
+ " cat ID: "
+ str(cat_id),
exception=err,
)
return None
def get_categories(self) -> list:
try:
cats = list()
for cat in self.category_labels:
if cat in self.categories.keys():
cats.append(cat)
return cats
except KeyError as err:
bu.error("Key not found", exception=err)
except Exception as err:
bu.error(exception=err)
return None
def get_filter_categories(self, filters: list) -> list:
try:
if type(filters) != list:
filters = [filters]
res = list()
for ndx in filters:
if ndx in range(0, len(self.category_labels)):
res.append(self.category_labels[ndx])
return res
except Exception as err:
bu.error("Category: " + str(self.category_key), exception=err)
return None
def find_bucket(self, value, bucket_breaks: list):
"""Find bucket for value"""
try:
max = len(bucket_breaks)
if max <= 1:
return 0
ndx = int(max // 2)
if value > bucket_breaks[ndx]:
return ndx + self.find_bucket(value, bucket_breaks[ndx:])
elif value < bucket_breaks[ndx]:
return self.find_bucket(value, bucket_breaks[:ndx])
else:
return ndx
except KeyError as err:
bu.error(
"Key not found ndx=" + str(ndx) + ", value=" + str(value), exception=err
)
except Exception as err:
bu.error("ndx=" + str(ndx) + ", value=" + str(value), exception=err)
return -2
def mk_category_labels(self, bucket_breaks: list, bucket_format: str) -> list:
try:
# add open-ended last category. list.append() modifies by reference self.breaks (not OK)!!
bucket_breaks = bucket_breaks + [None]
multiplier = 1
prefix = ""
if bucket_format == "%":
frmt = "{:2.0f}"
multiplier = 100
prefix = "%"
elif bucket_format == "float":
frmt = "{:.2f}"
elif bucket_format == "int":
frmt = "{:.0f}"
else:
frmt = "{:.0f}"
labels = list()
for ndx in range(0, len(bucket_breaks) - 1):
low = bucket_breaks[ndx]
high = bucket_breaks[ndx + 1]
# None makes open ended category label: '1500 - '
if low is None:
label = " - "
else:
label = frmt.format(low * multiplier) + " - "
if high is None:
label = label + " "
else:
label = label + frmt.format(high * multiplier)
label = label + prefix
labels.append(label)
return labels
except KeyError as err:
bu.error(
"KeyError: ndx=" + str(ndx) + " buckets=" + str(bucket_breaks),
exception=err,
)
except Exception as err:
bu.error("Unknown error", exception=err)
return None
class BattleCategory:
## Syntax: Check how the replay JSON files look. The algorithm is counting/recording fields
_result_fields = {
"battles": ["Battles", "Number of battles", 7, "{:7.0f}"],
"battles%": ["% Battles", "Share of Battles", 9, "{:9.0%}"],
"win": ["WR", "Win rate", 6, "{:6.1%}"],
"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}"],
"DR": ["DR", "Damage Ratio", 5, "{:5.1f}"],
"KDR": ["KDR", "Kills / Death", 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%}"],
"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_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%}"],
"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%}",
],
"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%}"],
"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%}",
],
"enemies_wins": [
"Enemies WR",
"Average WR of enemies at the tier of their tank",
10,
"{:10.2%}",
],
"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}",
],
"enemies_battles": [
"Enemies Btls",
"Average number battles of the enemies",
12,
"{:12.0f}",
],
"player_damage_dealt": [
"Player Avg Dmg",
"Player Average damage",
14,
"{:14.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",
15,
"{:14.0f}",
],
MISSING_STATS: ["No stats", "Players without stats avail", 8, "{:8.1%}"],
}
# fields to display in results
_result_fields_modes = {
"default": [
"battles",
"win",
"damage_made",
"enemies_destroyed",
"enemies_spotted",
"top_tier",
"DR",
"survived",
"allies_wins",
"enemies_wins",
],
"team": [
"battles",
"win",
"in_platoon",
"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_battles",
"allies_battles",
"enemies_battles",
],
"extended": [
"battles",
"battles%",
"win",
"in_platoon",
"damage_made",
"enemies_destroyed",
"enemies_spotted",
"top_tier",
"DR",
"KDR",
"hit_rate",
"pen_rate",
"survived",
"alive",
"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()],
}
_team_fields = ["wins", "battles", "damage_dealt"]
count_fields = ["battles", MISSING_STATS, "mastery_badge"]
_result_ratios = {
"KDR": ["enemies_destroyed", "destroyed"],
"DR": ["damage_made", "damage_received"],
"hit_rate": ["shots_hit", "shots_made"],
"splash_rate": ["shots_splash", "shots_hit"],
"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
result_fields = list()
avg_fields = set()
ratio_fields = set()
@classmethod
def get_modes(cls) -> list:
return list(cls._result_fields_modes.keys())
@classmethod
def get_mode_fields(cls, mode: str):
try:
return cls._result_fields_modes[mode]
except Exception as err:
bu.error(exception=err)
return None
@classmethod
def set_fields(cls, mode: str = None):
try:
if mode not in cls.get_modes():
bu.error(
"Mode "
+ mode
+ " not in defined modes: "
+ ", ".join(cls.get_modes())
)
sys.exit(2)
cls.mode = mode
cls.result_fields = cls.get_mode_fields(cls.mode)
cls.result_fields_ratio = set(cls._result_ratios.keys()) & set(
cls.result_fields
)
cls.avg_fields = (
set(cls.result_fields)
- set(cls._result_ratios.keys())
- set(cls.count_fields)
)
for ratio in cls.result_fields_ratio:
cls.ratio_fields.add(cls._result_ratios[ratio][0])
cls.ratio_fields.add(cls._result_ratios[ratio][1])
except Exception as err:
bu.error("BattleCategory:", exception=err)
@classmethod
def get_result_fields(cls) -> list:
return cls.result_fields
@classmethod
def get_result_fields_all(cls) -> list:
return cls._result_fields.keys()
@classmethod
def get_result_fields_ratio(cls, all: bool = False) -> list:
if all:
return cls._result_ratios.keys()
else:
return cls.result_fields_ratio
@classmethod
def get_avg_fields(cls) -> set:
return cls.avg_fields
@classmethod
def get_ratio_fields(cls) -> set:
return cls.ratio_fields
@classmethod
def get_field_name(cls, field: str) -> str:
try:
return cls._result_fields[field][0]
except Exception as 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
def get_field_width(cls, field: str):
try:
return cls._result_fields[field][2]
except Exception as err:
bu.error(exception=err)
@classmethod
def get_fields_team(cls) -> list:
return cls._team_fields
@classmethod
def calc_ratio(cls, ratio: str, result: dict):
try:
value = result[cls._result_ratios[ratio][0]]
divider = result[cls._result_ratios[ratio][1]]
if divider != 0:
if value is None:
value = 0
return value / divider
else:
if value == 0:
return 0
else:
return float("Inf")
except Exception as err:
bu.error("Replay _id=" + result["_id"], exception=err)
return None
@classmethod
def print_headers(cls, header: str):
try:
headers = [header]
for field in cls.result_fields:
print_format = "{:^" + str(cls.get_field_width(field)) + "s}"
headers.append(print_format.format(cls.get_field_name(field)))
print(" ".join(headers))
except KeyError as err:
bu.error("Key not found", exception=err)
except Exception as err:
bu.error(exception=err)
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
@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):
try:
self.battles = 0
self.missing_stats = 0
self.n_players = 0
self.ratios_ready = False
self.results_ready = False
self.results = collections.defaultdict(def_value_zero)
except KeyError as err:
bu.error("BattleCategory(): Key not found", err)
def record_result(self, result: dict) -> bool:
try:
for field in self.avg_fields | self.ratio_fields:
if (field in result) and (result[field] is not None):
self.results[field] += result[field]
self.battles += 1
self.n_players += result[N_PLAYERS]
self.missing_stats += result[MISSING_STATS]
return True
except KeyError as err:
bu.error("Key not found", err)
bu.error(str(result))
except Exception as err:
bu.error("BattleCategory:", exception=err)
return False
def calc_ratios(self) -> bool:
if self.ratios_ready:
return True
try:
for field in self.result_fields_ratio:
if self.results[self._result_ratios[field][1]] != 0:
self.results[field] = (
self.results[self._result_ratios[field][0]]
/ self.results[self._result_ratios[field][1]]
)
else:
self.results[field] = float("Inf")
self.ratios_ready = True
return True
except KeyError as err:
bu.error("Key not found", err)
return False
def calc_results(self, total_battles: int = None):
if self.results_ready == True:
return True
if not self.ratios_ready:
self.calc_ratios()
try:
for field in self.avg_fields:
self.results[field] = self.results[field] / max(self.battles, 1)
self.results["battles"] = self.battles
self.results["battles%"] = self.battles / total_battles
self.results[MISSING_STATS] = self.missing_stats / max(self.n_players, 1)
self.results_ready = True
return True
except KeyError as err:
bu.error("Key not found", err)
except Exception as err:
bu.error(exception=err)
return False
def get_results(self) -> dict:
"""Return results as a dict"""
if not self.results_ready:
bu.error(
"Stats have not been calculated yet. call calc_results() before get_results()"
)
try:
results = dict()
for field in self.result_fields:
results[field] = 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 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):
if not self.results_ready:
bu.error(
"Stats have not been calculated yet. call calc_results() before printing()"
)
try:
results = []
for field in self.result_fields:
results.append(
self._result_fields[field][3].format(self.results[field])
)
print(" : ".join(results))
return True
except KeyError as err:
bu.error("Key not found", err)
except Exception as err:
bu.error(exception=err)
return False
class PlayerHistogram:
def __init__(
self, field: str, name: str, fields: list, factor: float, cat_format: str
):
self.field = field
if self.field == "wins":
self.name = StatFunc.get_title()
else:
self.name = name
self.fields = fields
self.cat_factor = factor
self.cat_format = cat_format
self.ncat = len(self.fields) - 1
self.allies = [0] * self.ncat
self.enemies = [0] * self.ncat
self.total = [0] * self.ncat
self.results = None
def record_ally(self, stat: float):
for cat in range(0, self.ncat):
if self.fields[cat] <= stat <= self.fields[cat + 1]:
self.allies[cat] += 1
self.total[cat] += 1
return True
return False
def record_enemy(self, stat: float):
for cat in range(0, self.ncat):
if self.fields[cat] <= stat <= self.fields[cat + 1]:
self.enemies[cat] += 1
self.total[cat] += 1
return True
return False
def get_category_name(self, cat: int):
if cat < self.ncat - 1:
return self.format_category(cat) + " - " + self.format_category(cat + 1)
else:
return self.format_category(cat) + " -"
def format_category(self, cat: int):
val = self.fields[cat]
return self.cat_format.format(float(val * self.cat_factor))
def calc_results(self):
N_enemies = 0
N_allies = 0
res = dict()
# calculate buckets
for cat in range(0, self.ncat):
N_enemies += self.enemies[cat]
N_allies += self.allies[cat]
N_total = N_allies + N_enemies
for cat in range(0, self.ncat):
stat = dict()
stat["allies"] = self.allies[cat]
stat["allies%"] = stat["allies"] / N_allies
stat["enemies"] = self.enemies[cat]
stat["enemies%"] = stat["enemies"] / N_enemies
stat["total"] = self.allies[cat] + self.enemies[cat]
stat["total%"] = stat["total"] / N_total
res[self.get_category_name(cat)] = stat
self.results = res
return res
def get_results(self) -> dict:
return self.results
def get_results_list(self) -> list:
try:
if self.results is not 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 is not 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
2021-02-05 21:41:50 +02:00
class ErrorCatchingArgumentParser(argparse.ArgumentParser):
2023-09-10 20:00:23 +03:00
def exit(self, status=0, message=None):
if status:
if message is not None:
raise UserWarning(message)
else:
raise UserWarning()
2020-10-31 14:26:57 +02:00
2018-10-08 00:14:56 +08:00
## main() -------------------------------------------------------------
2023-09-10 20:00:23 +03:00
2018-10-08 00:14:56 +08:00
async def main(argv):
2023-09-10 20:00:23 +03:00
global wg, wi, WG_APP_ID, OPT_MODE_DEFAULT, OPT_EXPORT_CSV_FILE, OPT_EXPORT_JSON_FILE
# set the directory for the script
current_dir = os.getcwd()
os.chdir(os.path.dirname(sys.argv[0]))
2023-09-10 20:00:23 +03:00
## Default options:
OPT_MODE_DEFAULT = "default"
OPT_MODE_HELP = "help"
OPT_MODES = BattleCategory.get_modes() + [OPT_MODE_HELP]
OPT_DB = False
OPT_HIST = False
OPT_STAT_FUNC = StatFunc.get_default()
OPT_WORKERS_N = 10
# OPT_EXPORT_CSV_FILE = 'export.csv'
# OPT_EXPORT_JSON_FILE = 'export.json'
2020-11-03 23:57:10 +02:00
2023-09-10 20:00:23 +03:00
# 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
# WG_ID = None # WG account_id in integer format.
# WG_ACCOUNT will be used to retrieve the account_id, but it can be set directly too
WG_RATE_LIMIT = 10 ## WG standard. Do not edit unless you have your
## own server app ID, it will REDUCE the performance
2020-02-09 23:19:55 +02:00
2023-09-10 20:00:23 +03:00
## VERY unlikely you have a DB set up
DB_SERVER = "localhost"
DB_PORT = 27017
DB_TLS = False
DB_CERT_REQ = False
DB_AUTH = "admin"
DB_NAME = "BlitzStats"
DB_USER = "mongouser"
DB_PASSWD = "PASSWORD"
DB_CERT = None
DB_CA = None
2020-07-10 20:01:13 +03:00
2023-09-10 20:00:23 +03:00
try:
## Read config
if os.path.isfile(FILE_CONFIG):
config = configparser.ConfigParser()
config.read(FILE_CONFIG)
2020-07-10 20:01:13 +03:00
2023-09-10 20:00:23 +03:00
try:
if "ANALYZER" in config.sections():
configAnalyzer = config["ANALYZER"]
OPT_MODE_DEFAULT = configAnalyzer.getboolean(
"mode", OPT_MODE_DEFAULT
)
OPT_HIST = configAnalyzer.getboolean("histograms", OPT_HIST)
OPT_STAT_FUNC = configAnalyzer.get(
"stat_func", fallback=OPT_STAT_FUNC
)
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)
if res_categorizations is not None:
res_categorizations.replace(" ", "")
BattleCategorizationList.set_categorizations_default(
res_categorizations.split(",")
)
histogram_fields_str = configAnalyzer.get("histogram_buckets", None)
if histogram_fields_str is not None:
set_histogram_buckets(json.loads(histogram_fields_str))
2023-09-10 20:00:23 +03:00
except (KeyError, configparser.NoSectionError) as err:
bu.error(exception=err)
2023-09-10 20:00:23 +03:00
try:
if "WG" in config.sections():
configWG = config["WG"]
# WG account id of the uploader:
# # Find it here: https://developers.wargaming.net/reference/all/wotb/account/list/
# WG_ID = configWG.getint('wg_id', WG_ID)
# WG_ACCOUNT = configWG.get('wg_account', WG_ACCOUNT)
WG_APP_ID = configWG.get("wg_app_id", WG_APP_ID)
WG_RATE_LIMIT = configWG.getint("wg_rate_limit", WG_RATE_LIMIT)
except (KeyError, configparser.NoSectionError) as err:
bu.error(exception=err)
2023-09-10 20:00:23 +03:00
try:
if "DATABASE" in config.sections():
configDB = config["DATABASE"]
OPT_DB = configDB.getboolean("opt_DB", OPT_DB)
DB_SERVER = configDB.get("db_server", DB_SERVER)
DB_PORT = configDB.getint("db_port", DB_PORT)
DB_TLS = configDB.getboolean("db_tls", DB_TLS)
DB_CERT_REQ = configDB.getboolean("db_tls_req", DB_CERT_REQ)
DB_AUTH = configDB.get("db_auth", DB_AUTH)
DB_NAME = configDB.get("db_name", DB_NAME)
DB_USER = configDB.get("db_user", DB_USER)
DB_PASSWD = configDB.get("db_password", DB_PASSWD)
DB_CERT = configDB.get("db_tls_cert_file", DB_CERT)
DB_CA = configDB.get("db_tls_ca_file", DB_CA)
except (KeyError, configparser.NoSectionError) as err:
bu.error(exception=err)
2020-10-24 02:06:30 +03:00
2023-09-10 20:00:23 +03:00
parser = ErrorCatchingArgumentParser(
description="Analyze Blitz replay JSON files from WoTinspector.com. Use 'upload_wotb_replays.py' to upload the replay files first."
)
2020-10-25 00:31:17 +03:00
2023-09-10 20:00:23 +03:00
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(
"--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(
"--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('--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 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(
"--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(
"--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(
"--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(
"--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: : [ {"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(
"--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(
"-v", "--verbose", action="store_true", default=False, help="Verbose mode"
)
parser.add_argument(
"-s", "--silent", action="store_true", default=False, help="Silent mode"
)
parser.add_argument(
"-l",
"--log",
action="store_true",
default=False,
help="Enable file logging",
)
parser.add_argument(
"files",
metavar="FILE1 [FILE2 ...]",
type=str,
nargs="*",
help="Files/dirs to read. Use '-' for STDIN, \"db:\" for database",
)
## parser.add_argument('--help_filters', action='store_true', default=False, help='Extended help for --filters')
2020-10-25 00:31:17 +03:00
2023-09-10 20:00:23 +03:00
try:
args = parser.parse_args()
except Exception as err:
bu.error("Invalid arguments", exception=err)
sys.exit(0)
2023-09-10 20:00:23 +03:00
# res_categories = BattleCategorization.get_categorizations(OPT_CATEGORIZATIONS, args)
2023-09-10 20:00:23 +03:00
bu.set_log_level(args.silent, args.verbose, args.debug)
# bu.set_progress_step(250) # Set the frequency of the progress dots.
2020-11-03 22:46:44 +02:00
2023-09-10 20:00:23 +03:00
#### Connect to MongoDB. You are unlikely to have this set up...
bu.debug("DB_SERVER: " + DB_SERVER)
bu.debug("DB_PORT: " + str(DB_PORT))
bu.debug("DB_TLS: " + "True" if DB_TLS else "False")
bu.debug("DB_AUTH: " + DB_AUTH)
bu.debug("DB_NAME: " + DB_NAME)
2023-09-10 20:00:23 +03:00
client = None
db = None
if args.db:
try:
client = AsyncIOMotorClient(
DB_SERVER,
DB_PORT,
authSource=DB_AUTH,
username=DB_USER,
password=DB_PASSWD,
tls=DB_TLS,
tlsAllowInvalidCertificates=DB_CERT_REQ,
tlsCertificateKeyFile=DB_CERT,
tlsCAFile=DB_CA,
)
db = client[DB_NAME]
bu.debug("Database connection initiated")
except Exception as err:
bu.error("Could no initiate DB connection: Disabling DB", err)
args.db = False
else:
bu.debug("No DB in use")
2023-09-10 20:00:23 +03:00
# TBD
if args.mode == OPT_MODE_HELP:
await help_extended(db, parser)
sys.exit(0)
elif len(args.files) == 0:
raise UserWarning("No FILES argument given: No replays to analyse")
2020-11-03 22:46:44 +02:00
2023-09-10 20:00:23 +03:00
if args.log:
await bu.set_file_logging("analyze_wotb_replays", add_timestamp=True)
2020-11-03 22:46:44 +02:00
2023-09-10 20:00:23 +03:00
StatFunc.set_stat_func(args.stat_func)
# rebase file arguments due to moving the working directory to the script location
args.files = bu.rebase_file_args(current_dir, args.files)
except Exception as err:
bu.error(err)
sys.exit(1)
2023-09-10 20:00:23 +03:00
try:
wg = WG(
WG_app_id=WG_APP_ID,
tankopedia_fn=args.tankfile,
maps_fn=args.mapfile,
stats_cache=True,
rate_limit=WG_RATE_LIMIT,
)
wi = WoTinspector(rate_limit=10)
2023-09-10 20:00:23 +03:00
if args.account is not None:
args.account_id = await wg.get_account_id(args.account)
bu.debug("WG account_id: " + str(args.account_id))
2023-09-10 20:00:23 +03:00
# try:
BattleCategory.set_fields(args.mode)
replayQ = asyncio.Queue(maxsize=1000)
reader_tasks = []
# Make replay Queue
2023-09-10 20:00:23 +03:00
scanner_task = asyncio.create_task(mk_replayQ(replayQ, args, db))
bu.set_counter("Reading replays", 10)
# Start tasks to process the Queue
for i in range(OPT_WORKERS_N):
reader_tasks.append(asyncio.create_task(replay_reader(replayQ, i, args)))
bu.debug("ReplayReader " + str(i) + " started")
2023-09-10 20:00:23 +03:00
bu.debug("Waiting for the replay scanner to finish")
await asyncio.wait([scanner_task])
bu.finish_progress_bar()
# bu.debug('Scanner finished. Waiting for replay readers to finish the queue')
await replayQ.join()
await asyncio.sleep(0.1)
bu.debug("Replays read. Cancelling Readers and analyzing results")
for task in reader_tasks:
task.cancel()
await asyncio.sleep(0.1)
results = []
players = set()
replays = dict()
for res in await asyncio.gather(*reader_tasks):
results.extend(res[0])
players.update(res[1])
replays.update(res[2])
2023-09-10 20:00:23 +03:00
if len(players) == 0:
raise Exception("No players found to fetch stats for. No replays found?")
if args.min is not None:
results = filter_min_replays_by_player(results, args.min)
# Filter based on non-stats filters
results = filter_results(results, args.filters, stats_filters=False)
players = get_players(results)
(player_stats, stat_id_map) = await process_player_stats(
players, OPT_WORKERS_N, args, db
)
# Filter based on stats filters
results = filter_results(results, args.filters, stats_filters=True)
bu.debug(str(len(replays)) + " replays to process")
replays = filter_replays(replays, results)
bu.debug("Number of player stats: " + str(len(player_stats)))
teamresults = calc_team_stats(results, player_stats, stat_id_map, args)
blt_cat_list = process_battle_results(teamresults, args)
bu.verbose_std("WR = " + StatFunc.get_title())
blt_cat_list.print_results()
histograms = None
if args.hist:
histograms = process_player_dist(results, player_stats, stat_id_map)
print_player_dist(histograms)
if args.json:
await export_json(args, blt_cat_list, histograms)
if args.csv:
await export_csv(args, blt_cat_list, histograms)
if args.replays:
await export_replays(replays)
bu.debug("Finished. Cleaning up..................")
# except Exception as err:
# bu.error(exception=err)
except UserWarning as err:
bu.warning(str(err))
except asyncio.CancelledError:
pass
except Exception as err:
bu.error(exception=err)
finally:
## Need to close the aiohttp.session since Python destructors do not support async methods...
if wg is not None:
await wg.close()
if wi is not None:
await wi.close()
return None
2018-10-08 00:14:56 +08:00
2020-07-10 20:01:13 +03:00
2023-09-10 20:00:23 +03:00
async def export_csv(
args: argparse.Namespace,
blt_cat_list: BattleCategorizationList,
histograms: dict = None,
):
"""Export results into a CSV file"""
try:
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)
except Exception as err:
bu.error(exception=err)
2023-09-10 20:00:23 +03:00
async def export_json(
args: argparse.Namespace,
blt_cat_list: BattleCategorizationList,
histograms: dict = None,
):
"""Export results into a JSON file"""
try:
res = blt_cat_list.get_results_json()
if histograms is not None:
res_hist = dict()
for histogram in histograms.keys():
res_hist[histogram] = histograms[histogram].get_results()
res["histograms"] = res_hist
if args.outfile == "-":
export_file = OPT_EXPORT_JSON_FILE
else:
export_file = 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)
except Exception as err:
bu.error(exception=err)
2023-09-10 20:00:23 +03:00
async def help_extended(
db: AsyncIOMotorDatabase = None,
parser: argparse.ArgumentParser = None,
):
"""Help for --filters"""
if parser is not None:
parser.print_help()
# Result category options
2020-10-28 09:35:16 +02:00
2023-09-10 20:00:23 +03:00
# Results fields
print("\n-------------------------------------------------------------------")
print("| Result fields |")
print("-------------------------------------------------------------------\n")
2023-09-10 20:00:23 +03:00
BattleCategory.help()
2023-09-10 20:00:23 +03:00
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
print("\n-------------------------------------------------------------------")
print("| DB Filter usage |")
print("-------------------------------------------------------------------\n")
print(
'You are unlikely to have DB setup required for these filters. This is "Jylpah\'s special"'
)
print("")
print(
'Syntax: --filters_db \'{ "replay.param": VALUE, "replay.param2": { "$MONGO_OPERATOR" : VALUE }, ... }\''
)
print(
'\tExample: --filters_db \'{ "data.summary.protagonist": ACCOUNT_ID, "data.summary.battle_start_timestamp": { "$gt" : 1602853200 } }\''
)
if db is not None:
dbc = db[DB_C_REPLAYS]
try:
res = await dbc.find_one()
summary = res["data"]["summary"]
details = summary["details"][0]
print("\n--filters KEYS:")
for s in summary:
print("\tdata.summary." + s)
for d in details:
print("\tdata.summary.details.[]." + d)
except Exception as err:
bu.error(exception=err)
2020-10-24 02:06:30 +03:00
2020-11-24 22:49:03 +02:00
async def export_replays(replays: dict):
2023-09-10 20:00:23 +03:00
"""Export replays"""
global wg
try:
replay_dir = OPT_EXPORT_REPLAY_DIR + "_" + bu.get_date_str()
bu.verbose_std("Exporting replays to " + replay_dir)
if not os.path.isdir(replay_dir):
os.mkdir(replay_dir)
2023-09-10 20:00:23 +03:00
i = 0
for replay in replays.values():
filename = wg.get_replay_filename(replay)
async with aiofiles.open(
os.path.join(replay_dir, filename), "w", encoding="utf8"
) as f:
await f.write(json.dumps(replay, indent=4))
i += 1
bu.verbose_std(str(i) + " replays exported")
except Exception as err:
bu.error(exception=err)
2020-11-03 22:46:44 +02:00
## move the class PlayerHistogram?
2020-07-10 20:01:13 +03:00
def set_histogram_buckets(json: dict):
2023-09-10 20:00:23 +03:00
global histogram_fields
try:
for field in json.keys():
histogram_fields[field][1] = json[field]
except (KeyError, Exception) as err:
bu.error(exception=err)
return histogram_fields
2020-07-10 20:01:13 +03:00
2020-11-03 22:46:44 +02:00
def get_players(results: list) -> set:
2023-09-10 20:00:23 +03:00
try:
players = set()
for res in results:
players.add(res["player"])
players.update(res["allies"])
players.update(res["enemies"])
return players
except Exception as err:
bu.error(exception=err)
return None
2020-11-03 22:46:44 +02:00
def filter_min_replays_by_player(results: list, min_replays: int) -> list:
2023-09-10 20:00:23 +03:00
try:
replay_counter = collections.defaultdict(def_value_zero)
for res in results:
replay_counter[res["protagonist"]] += 1
2020-11-03 22:46:44 +02:00
2023-09-10 20:00:23 +03:00
players_enough_replays = set()
for player in replay_counter.keys():
if replay_counter[player] >= min_replays:
players_enough_replays.add(player)
res_ret = list()
for res in results:
if res["protagonist"] in players_enough_replays:
res_ret.append(res)
bu.verbose_std("Replays after filtering: " + str(len(res_ret)))
return res_ret
except Exception as err:
bu.error(exception=err)
return None
2020-11-03 22:46:44 +02:00
2021-02-05 19:32:39 +02:00
def process_player_dist(results: list, player_stats: dict, stat_id_map: dict) -> dict:
2023-09-10 20:00:23 +03:00
"""Process player distribution"""
hist_stats = dict()
try:
for field in histogram_fields:
hist_stats[field] = PlayerHistogram(
field,
histogram_fields[field][0],
histogram_fields[field][1],
histogram_fields[field][2],
histogram_fields[field][3],
)
2023-09-10 20:00:23 +03:00
for result in results:
for player in result["allies"] | result["enemies"]: # union of Sets
player_remapped = stat_id_map[player]
if player_remapped in player_stats:
if player in result["allies"]:
for stat_field in hist_stats:
hist_stats[stat_field].record_ally(
player_stats[player_remapped][stat_field]
)
else:
for stat_field in hist_stats:
hist_stats[stat_field].record_enemy(
player_stats[player_remapped][stat_field]
)
2021-02-05 19:32:39 +02:00
2023-09-10 20:00:23 +03:00
for stat_field in hist_stats:
hist_stats[stat_field].calc_results()
2023-09-10 20:00:23 +03:00
return hist_stats
except Exception as err:
bu.error(exception=err)
return None
2021-02-05 19:32:39 +02:00
def print_player_dist(histograms: dict):
2023-09-10 20:00:23 +03:00
# Print results
print("\nPlayer Histograms______", end="", flush=True)
for hist in histograms.values():
print("")
hist.print()
2021-02-05 19:32:39 +02:00
2021-11-23 18:38:49 +02:00
2023-09-10 20:00:23 +03:00
def process_battle_results(results: dict, args: argparse.Namespace):
"""Process replay battle results"""
try:
url = args.url
cats = BattleCategorizationList.get_categorizations(args)
blt_cat_list = None
2023-09-10 20:00:23 +03:00
# remove if?
if len(results) > 0:
blt_cat_list = BattleCategorizationList(cats)
for result in results:
blt_cat_list.record_result(result)
if url:
blt_cat_list.record_url(result)
2023-09-10 20:00:23 +03:00
blt_cat_list.calc_results()
else:
warning_txt = "No replays to process "
if args.filters is not None:
warning_txt = warning_txt + "after filtering"
bu.warning(warning_txt)
except Exception as err:
bu.error(exception=err)
return blt_cat_list
2018-10-08 00:14:56 +08:00
2023-09-10 20:00:23 +03:00
def filter_results(results: list, filters_json: str, stats_filters=None) -> bool:
"""Filter replays based on battle category filters"""
try:
if filters_json is None:
return results
filters = json.loads(filters_json)
bu.debug(str(filters))
if type(filters) != dict:
bu.error("invalid --filter arguments: " + str(filter))
sys.exit(1)
2023-09-10 20:00:23 +03:00
filter_cats = BattleCategorizationList.get_categorizations_stats(
filters.keys(), stats_filters
)
filter_cats = BattleCategorizationList(filter_cats)
if filter_cats.len() == 0:
bu.debug("Nothing to filter")
return results
2021-08-03 19:14:44 +03:00
2023-09-10 20:00:23 +03:00
bu.verbose("Filters applied:")
for category in filter_cats:
cat = category.category_key
categorization = filter_cats.get_categorization(cat)
bu.debug("Category: " + cat + " filters: " + str(filters))
filters[cat] = categorization.get_filter_categories(filters[cat])
bu.verbose(" " + cat + " = " + ",".join(filters[cat]))
bu.verbose("")
res = list()
for result in results:
try:
cats = filter_cats.get_categories(result)
bu.debug(str(cats))
ret = False
for cat in cats:
if cats[cat] not in filters[cat]:
ret = False
break
ret = True
if ret:
res.append(result)
except Exception as err:
bu.error(exception=err)
if len(res) == 0:
raise UserWarning("No replays left after filtering")
bu.verbose_std("Replays after filtering: " + str(len(res)))
return res
except UserWarning:
raise
except Exception as err:
bu.error(exception=err)
return None
2020-11-24 23:47:19 +02:00
2021-05-12 00:04:36 +03:00
def filter_replays(replays_in: dict, results: list) -> list:
2023-09-10 20:00:23 +03:00
"""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
2021-05-11 14:14:10 +03:00
2023-09-10 20:00:23 +03:00
async def process_player_stats(
players,
N_workers: int,
args: argparse.Namespace,
db: AsyncIOMotorDatabase,
) -> dict:
"""Start stat_workers to retrieve and store players' stats"""
## Create queue of player-tank pairs to find stats for
try:
statsQ = asyncio.Queue()
bu.debug("Create player stats queue: " + str(len(players)) + " players")
stat_id_map = dict()
stat_ids = set()
2023-09-10 20:00:23 +03:00
stat_id_map_func = globals()[StatFunc.get_stat_id_func()]
2023-09-10 20:00:23 +03:00
for player in players:
stat_id_map[player] = stat_id_map_func(player)
stat_ids.add(stat_id_map[player])
2020-02-15 15:26:32 +02:00
2023-09-10 20:00:23 +03:00
bu.set_progress_bar("Fetching player stats", len(stat_ids), 25, True)
# create statsQ
for stat_id in stat_ids:
await statsQ.put(stat_id)
2023-09-10 20:00:23 +03:00
# Process player WR / battle stats
stats_tasks = []
bu.debug("Starting player stats workers")
for i in range(N_workers):
bu.debug("Starting worker " + str(i))
stats_tasks.append(asyncio.create_task(stat_worker(statsQ, i, args, db)))
2018-10-08 00:14:56 +08:00
2023-09-10 20:00:23 +03:00
bu.debug("Waiting stats workers to finish")
await statsQ.join()
bu.debug("Cancelling stats workers")
for task in stats_tasks:
task.cancel()
player_stats = dict()
stat_id_remap = dict()
2023-09-10 20:00:23 +03:00
bu.debug("Gathering stats worker outputs")
for stats, id_remap in await asyncio.gather(*stats_tasks):
player_stats = {**player_stats, **stats}
stat_id_remap = {**stat_id_remap, **id_remap}
bu.finish_progress_bar()
## DO REMAPPING
stat_id_map = remap_stat_id(stat_id_map, stat_id_remap)
bu.debug("Returning player_stats")
return (player_stats, stat_id_map)
except Exception as err:
bu.error(exception=err)
return None
def remap_stat_id(stat_id_map: dict, stat_id_remap: dict):
2023-09-10 20:00:23 +03:00
bu.debug(
"Remapping stat_ids: stat_id_map: "
+ str(len(stat_id_map))
+ " remap needed: "
+ str(len(stat_id_remap))
)
for stat_id in stat_id_map:
# bu.debug(stat_id)
if stat_id_map[stat_id] in stat_id_remap.keys():
stat_id_map[stat_id] = stat_id_remap[stat_id_map[stat_id]]
return stat_id_map
2023-09-10 20:00:23 +03:00
def calc_team_stats(
results: list, player_stats: dict, stat_id_map: dict, args: argparse.Namespace
) -> list:
"""Calculate team stats"""
2023-09-10 20:00:23 +03:00
return_list = []
2020-02-15 15:26:32 +02:00
2023-09-10 20:00:23 +03:00
## Bug here??
# stat_types = player_stats[list(player_stats.keys())[0]].keys()
# stat_types = list()
stat_types = BattleCategory.get_fields_team()
2023-09-10 20:00:23 +03:00
for result in results:
try:
missing_stats = 0
n_players = len(result["allies"]) + len(result["enemies"])
n_allies = collections.defaultdict(def_value_zero)
allies_stats = collections.defaultdict(def_value_zero)
n_enemies = collections.defaultdict(def_value_zero)
enemies_stats = collections.defaultdict(def_value_zero)
# bu.debug('Processing Allies')
for ally in result["allies"]:
# Player itself is not in 'allies': see read_replay_JSON()
ally_mapped = stat_id_map[ally]
if ally_mapped not in player_stats.keys():
missing_stats += 1
continue
for stat in stat_types:
if player_stats[ally_mapped][stat] is not None:
allies_stats[stat] += player_stats[ally_mapped][stat]
n_allies[stat] += 1
# bu.debug('Processing Enemies')
for enemy in result["enemies"]:
enemy_mapped = stat_id_map[enemy]
if enemy_mapped not in player_stats:
missing_stats += 1
continue
for stat in stat_types:
if player_stats[enemy_mapped][stat] is not None:
enemies_stats[stat] += player_stats[enemy_mapped][stat]
n_enemies[stat] += 1
# bu.debug('Processing player's own stats')
player_mapped = stat_id_map[result["player"]]
if player_mapped not in player_stats:
missing_stats += 1
else:
for stat in stat_types:
if player_stats[player_mapped][stat] is not None:
result["player_" + stat] = player_stats[player_mapped][stat]
for stat in stat_types:
if n_allies[stat] > 0:
result["allies_" + stat] = allies_stats[stat] / n_allies[stat]
else:
bu.debug("No allies stats for: " + str(result))
if n_enemies[stat] > 0:
result["enemies_" + stat] = enemies_stats[stat] / n_enemies[stat]
else:
bu.debug("No enemies stats for: " + str(result))
# Steamroller stats
result["team_result"] = (
str(result["allies_survived"]) + "-" + str(result["enemies_survived"])
)
result[N_PLAYERS] = n_players
result[MISSING_STATS] = missing_stats
return_list.append(result)
except KeyError as err:
bu.error("Key not found", err)
except Exception as err:
bu.error(exception=err)
return return_list
2018-10-08 00:14:56 +08:00
2023-09-10 20:00:23 +03:00
async def stat_worker(
queue: asyncio.Queue,
workerID: int,
args: argparse.Namespace,
db: AsyncIOMotorDatabase,
) -> list:
"""Worker thread to find stats for player / tank pairs"""
global wg
stats = dict()
stat_id_remap = dict()
2023-09-10 20:00:23 +03:00
stat_db_func = globals()[StatFunc.get_db_func()]
stat_wg_func = globals()[StatFunc.get_wg_func()]
2023-09-10 20:00:23 +03:00
bu.debug("workedID: " + str(workerID) + " started", id=workerID)
try:
while True:
stat_id = await queue.get()
try:
bu.debug("Stat_id: " + stat_id, id=workerID)
bu.print_progress()
stats_tmp = None
2023-09-10 20:00:23 +03:00
# Try cache first
pruned_stat_id = prune_stat_id(stat_id)
if pruned_stat_id not in stats:
stats_tmp = await stat_wg_func(pruned_stat_id, cache_only=True)
else:
stat_id_remap[stat_id] = pruned_stat_id
continue
2023-09-10 20:00:23 +03:00
if stats_tmp is not None:
stats[pruned_stat_id] = stats_tmp
stat_id_remap[stat_id] = pruned_stat_id
continue
2020-11-24 22:49:03 +02:00
2023-09-10 20:00:23 +03:00
# try DB
stats[stat_id] = await stat_db_func(db, stat_id)
bu.debug(
"get_db_"
+ args.stat_func
+ "_stats returned: account_id="
+ str(get_account_id_f_stat_id(stat_id))
+ ": "
+ str(stats[stat_id]),
workerID,
)
# no DB stats found, trying WG AP
if stats[stat_id] is None:
stats[pruned_stat_id] = await stat_wg_func(pruned_stat_id)
stat_id_remap[stat_id] = pruned_stat_id
del stats[stat_id]
bu.debug(
"get_wg_"
+ args.stat_func
+ "_stats returned: account_id="
+ str(get_account_id_f_stat_id(pruned_stat_id))
+ ": "
+ str(stats[pruned_stat_id]),
workerID,
)
except KeyError as err:
bu.error("Key not found", err, id=workerID)
except Exception as err:
bu.error("Unexpected error", err, id=workerID)
finally:
queue.task_done()
except (asyncio.CancelledError, concurrent.futures.CancelledError):
bu.debug("Stats queue is empty", id=workerID)
except Exception as err:
bu.error(exception=err, id=workerID)
finally:
# remove empty stats
try:
keys_2_del = []
for key in stats:
if stats[key] is None:
keys_2_del.append(key)
for key in keys_2_del:
del stats[key]
except KeyError as err:
bu.error("Error in pruning empty stats", err, id=workerID)
# bu.debug('Returning stats & exiting')
return (stats, stat_id_remap)
2018-10-08 00:14:56 +08:00
## player stat functions: tank
2023-09-10 20:00:23 +03:00
async def get_wg_tank_stats(stat_id_str: str, cache_only=False) -> dict:
"""Get player stats from WG. Returns WR per tier of tank_id"""
global wg
try:
(account_id, tank_id) = str2ints(stat_id_str)
2023-09-10 20:00:23 +03:00
# 'battles' must always be there
hist_stats = ["all." + x for x in histogram_fields.keys()]
hist_stats.append("tank_id")
2023-09-10 20:00:23 +03:00
player_stats = await wg.get_player_tank_stats(
account_id, [tank_id], hist_stats, cache_only=cache_only
)
# bu.debug('account_id: ' + str(account_id) + ' ' + str(player_stats))
2023-09-10 20:00:23 +03:00
return tank_stats_helper(player_stats)
except KeyError as err:
bu.error(
"account_id: "
+ str(account_id)
+ " tank_id:"
+ str(tank_id)
+ " : Key not found",
err,
)
except Exception as err:
bu.error(exception=err)
return None
2023-09-10 20:00:23 +03:00
async def get_db_tank_stats(db: 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 is None:
return None
try:
dbc = db[DB_C_TANK_STATS]
(account_id, tank_id, battletime) = str2ints(stat_id_str)
time_buffer = 2 * 7 * 24 * 3600
2020-11-24 22:49:03 +02:00
2023-09-10 20:00:23 +03:00
hist_stats = ["all." + x for x in histogram_fields.keys()]
hist_stats.append("tank_id")
hist_stats.append("account_id")
hist_stats.append("last_battle_time")
project = dict()
for stat in hist_stats:
project[stat] = True
project["_id"] = False
bu.debug(
"find(): tank_id={} account_id={} last_battle_time>={}".format(
tank_id, account_id, battletime - time_buffer
)
)
cursor = (
dbc.find(
{
"$and": [
{"tank_id": tank_id},
{"account_id": account_id},
{"last_battle_time": {"$gte": battletime - time_buffer}},
]
},
projection=project,
)
.sort("last_battle_time", -1)
.limit(1)
)
2023-09-10 20:00:23 +03:00
# pipeline = [ { '$match': { '$and': [ { 'account_id': account_id }, { 'last_battle_time': { '$lte': battletime + time_buffer }}, { 'tank_id' : tank_id } ]}},
# { '$sort': { 'last_battle_time': -1 }},
# { '$group': { '_id': '$tank_id', 'doc': { '$first': '$$ROOT' }}},
# { '$replaceRoot': { 'newRoot': '$doc' }},
# { '$project': { '_id': 0 }} ]
2023-09-10 20:00:23 +03:00
# cursor = dbc.aggregate(pipeline, allowDiskUse=True)
# cursor = dbc.aggregate(pipeline)
2023-09-10 20:00:23 +03:00
stats = await cursor.to_list(1)
# store cache
# await wg.save_tank_stats(account_id, [tank_id], stats)
return tank_stats_helper(stats)
except Exception as err:
bu.error("account_id: " + str(account_id) + " Error", err)
return None
## player stat functions: tank_tier
2023-09-10 20:00:23 +03:00
async def get_wg_tank_tier_stats(stat_id_str: str, cache_only: bool = False) -> dict:
"""Get player stats from WG. Returns WR per tier of tank_id"""
global wg
try:
(account_id, tier) = str2ints(stat_id_str)
tier_tanks = wg.get_tanks_by_tier(tier)
2023-09-10 20:00:23 +03:00
# 'battles' must always be there
hist_stats = ["all." + x for x in histogram_fields.keys()]
hist_stats.append("tank_id")
2023-09-10 20:00:23 +03:00
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))
2023-09-10 20:00:23 +03:00
return tank_stats_helper(player_stats)
2023-09-10 20:00:23 +03:00
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
## player stat functions: tier_x
2023-09-10 20:00:23 +03:00
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"""
global wg
try:
account_id = int(stat_id_str)
tier_tanks = wg.get_tanks_by_tier(10)
2023-09-10 20:00:23 +03:00
# 'battles' must always be there
hist_stats = ["all." + x for x in histogram_fields.keys()]
hist_stats.append("tank_id")
2023-09-10 20:00:23 +03:00
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))
2023-09-10 20:00:23 +03:00
return tank_stats_helper(player_stats)
2023-09-10 20:00:23 +03:00
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
2023-09-10 20:00:23 +03:00
async def get_db_tank_tier_stats(db: AsyncIOMotorDatabase, stat_id_str: str) -> dict:
"""Get player stats from MongoDB (you are very unlikely to have it, unless you have set it up)"""
global wg
if db is None:
return None
try:
dbc = db[DB_C_TANK_STATS]
(account_id, tier, battletime) = str2ints(stat_id_str)
tier_tanks = wg.get_tanks_by_tier(tier)
time_buffer = 2 * 7 * 24 * 3600
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)
stats = await cursor.to_list(1000)
# await wg.save_tank_stats(account_id, tier_tanks, stats)
return tank_stats_helper(stats)
except Exception as err:
bu.error("account_id: " + str(account_id) + " Error", err)
return None
async def get_db_tier_x_stats(db: 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 is None:
return None
try:
(account_id, battletime) = str2ints(stat_id_str)
return await get_db_tank_tier_stats(db, get_stat_id(account_id, 10, battletime))
except Exception as err:
bu.error("account_id: " + str(account_id) + " Error", err)
return None
## player stat functions: player
2023-09-10 20:00:23 +03:00
async def get_db_player_stats(db: 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 is None:
return None
try:
dbc = db[DB_C_TANK_STATS] ## not collecting player stats yet 2019-12-23
(account_id, battletime) = str2ints(stat_id_str)
time_buffer = 2 * 7 * 24 * 3600
2023-09-10 20:00:23 +03:00
pipeline = [
{
"$match": {
"$and": [
{"account_id": account_id},
{"last_battle_time": {"$lte": battletime + time_buffer}},
]
}
},
{"$sort": {"last_battle_time": -1}},
{"$group": {"_id": {"tank_id": "$tank_id"}, "doc": {"$first": "$$ROOT"}}},
{"$replaceRoot": {"newRoot": "$doc"}},
{"$project": {"_id": 0}},
]
2023-09-10 20:00:23 +03:00
# cursor = dbc.aggregate(pipeline, allowDiskUse=True)
cursor = dbc.aggregate(pipeline)
stats = await cursor.to_list(1000)
# await wg.save_tank_stats(account_id, [], stats)
return tank_stats_helper(stats)
except Exception as err:
bu.error("account_id: " + str(account_id) + " Error", err)
return None
2023-09-10 20:00:23 +03:00
async def get_wg_player_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)
2023-09-10 20:00:23 +03:00
# 'battles' must always be there
hist_stats = ["statistics.all." + x for x in histogram_fields.keys()]
hist_stats.append("account_id")
2023-09-10 20:00:23 +03:00
player_stats = await wg.get_player_stats(
account_id, hist_stats, cache_only=cache_only
)
# bu.debug('account_id: ' + str(account_id) + ' ' + str(player_stats))
2023-09-10 20:00:23 +03:00
return await player_stats_helper(player_stats)
except KeyError as err:
bu.error("account_id: " + str(account_id) + " Key not found", err)
except Exception as err:
bu.error(exception=err)
return None
2021-04-17 14:34:05 +03:00
def tank_stats_helper(stat_list: list):
2023-09-10 20:00:23 +03:00
"""Helpher func got get_db_tank_tier_stats() and get_wg_tank_tier_stats()"""
try:
if stat_list is None:
return None
stats = collections.defaultdict(def_value_zero)
hist_fields = histogram_fields.keys()
if "battles" not in hist_fields:
bu.error("'battles' must be defined in 'histogram_fields'")
return None
2023-09-10 20:00:23 +03:00
for tank_stat in stat_list:
tank_stat = tank_stat["all"]
for field in hist_fields:
stats[field] += max(0, tank_stat[field])
2023-09-10 20:00:23 +03:00
if stats["battles"] == 0:
return None
2023-09-10 20:00:23 +03:00
for field in hist_fields:
if field == "battles":
continue
stats[field] = stats[field] / stats["battles"]
return stats
2023-09-10 20:00:23 +03:00
except Exception as err:
bu.error(exception=err)
return None
async def player_stats_helper(player_stats: dict):
2023-09-10 20:00:23 +03:00
"""Extract relevant stats for player histogram from raw player stats"""
try:
if player_stats is None:
return None
stats = dict()
hist_fields = histogram_fields.keys()
if "battles" not in hist_fields:
bu.error("'battles' must be defined in 'histogram_fields'")
return None
2023-09-10 20:00:23 +03:00
for field in hist_fields:
stats[field] = max(0, player_stats["statistics"]["all"][field])
2023-09-10 20:00:23 +03:00
if stats["battles"] == 0:
return None
2023-09-10 20:00:23 +03:00
for field in hist_fields:
if field == "battles":
continue
stats[field] = stats[field] / stats["battles"]
return stats
except Exception as err:
bu.error(exception=err)
return None
2023-09-10 20:00:23 +03:00
async def mk_replayQ(
queue: asyncio.Queue,
args: argparse.Namespace,
db: AsyncIOMotorDatabase = None,
):
"""Create queue of replays to post"""
p_replayfile = re.compile(".*\\.wotbreplay\\.json$")
files = args.files
Nreplays = 0
if files[0] == "db:":
if db is None:
bu.error("No database connection opened")
sys.exit(1)
try:
dbc = db[DB_C_REPLAYS]
cursor = None
if args.filters_db is not None:
bu.debug(str(args.filters_db))
filters = json.loads(args.filters_db)
bu.debug(json.dumps(filters, indent=2))
cursor = dbc.find(filters)
else:
# select all
cursor = dbc.find(dict())
bu.debug("Reading replays...")
async for replay_json in cursor:
_id = replay_json["_id"]
# del(replay_json['_id'])
await queue.put(await mk_readerQ_item(replay_json, _id=_id))
Nreplays += 1
bu.debug("All the matching replays have been read from the DB")
except Exception as err:
bu.error(exception=err)
2020-02-09 23:19:55 +02:00
2023-09-10 20:00:23 +03:00
elif files[0] == "-":
bu.debug("reading replay file list from STDIN")
stdin, _ = await aioconsole.get_standard_streams()
while True:
try:
line = (await stdin.readline()).decode("utf-8").rstrip()
if not line:
break
if p_replayfile.match(line) is not None:
replay_json = await bu.open_JSON(line, wi.chk_JSON_replay)
await queue.put(await mk_readerQ_item(replay_json, filename=line))
except Exception as err:
bu.error(exception=err)
else:
for fn in files:
try:
# bu.debug('Filename: ' + fn)
if fn.endswith('"'):
fn = fn[:-1]
if os.path.isfile(fn) and (p_replayfile.match(fn) is not None):
replay_json = await bu.open_JSON(fn, wi.chk_JSON_replay)
await queue.put(await mk_readerQ_item(replay_json, filename=fn))
bu.debug("File added to queue: " + fn)
Nreplays += 1
elif os.path.isdir(fn):
with os.scandir(fn) as dirEntry:
for entry in dirEntry:
if entry.is_file() and (
p_replayfile.match(entry.name) is not None
):
bu.debug(entry.name)
replay_json = await bu.open_JSON(
entry.path, wi.chk_JSON_replay
)
await queue.put(
await mk_readerQ_item(
replay_json, filename=entry.name
)
)
bu.debug("File added to queue: " + entry.path)
Nreplays += 1
except Exception as err:
bu.error(exception=err)
return Nreplays
2018-10-08 00:14:56 +08:00
2023-09-10 20:00:23 +03:00
async def mk_readerQ_item(replay_json, filename: str = None, _id: str = None) -> list:
"""Make an item to replay queue"""
global REPLAY_N
REPLAY_N += 1
try:
if wi.chk_JSON_replay(replay_json):
if not "_id" in replay_json:
if _id is None:
_id = wi.read_replay_id(replay_json)
replay_json["_id"] = _id
else:
replay_json = None # mark error
except Exception as err:
bu.error(exception=err)
if filename is not None:
return [replay_json, REPLAY_N, os.path.basename(filename)]
elif _id is not None:
return [replay_json, REPLAY_N, "DB: " + _id]
else:
return None
2018-10-08 00:14:56 +08:00
2023-09-10 20:00:23 +03:00
async def replay_reader(queue: asyncio.Queue, readerID: int, args: argparse.Namespace):
"""Async Worker to process the replay queue"""
# global SKIPPED_N
global wi
results = []
playerstanks = set()
replays = dict()
try:
while True:
item = await queue.get()
replay_json = item[0]
replayID = item[1]
replay_file = item[2]
2018-10-08 00:14:56 +08:00
2023-09-10 20:00:23 +03:00
try:
# msg_str = 'Replay[' + str(replayID) + ']: '
if replay_json is None:
bu.warning(
"Invalid replay. Skipping: "
+ (replay_file if replay_file is not None else "")
)
queue.task_done()
continue
2021-05-12 00:04:36 +03:00
2023-09-10 20:00:23 +03:00
## Read the replay JSON
bu.debug("reading replay", readerID)
result = await read_replay_JSON(replay_json, args)
bu.print_progress()
if result is None:
bu.warning(
"Invalid replay "
+ (replay_file if replay_file is not None else "")
)
queue.task_done()
continue
playerstanks.update(set(result["allies"]))
playerstanks.update(set(result["enemies"]))
playerstanks.update(set([result["player"]]))
_id = wi.read_replay_id(replay_json)
replays[_id] = replay_json
results.append(result)
bu.debug("Marking replay [" + str(replayID) + "] done", id=readerID)
except Exception as err:
bu.error(exception=err)
queue.task_done()
except (asyncio.CancelledError, concurrent.futures.CancelledError):
bu.debug(
str(len(results)) + " replays, " + str(len(playerstanks)) + " player/tanks",
readerID,
)
return results, playerstanks, replays
return None
2018-10-08 00:14:56 +08:00
2023-09-10 20:00:23 +03:00
async def read_replay_JSON(replay_json: dict, args: argparse.Namespace) -> dict:
"""Parse replay JSON dict"""
global REPLAY_I, wg
account_id = args.account_id
url = args.url
# db = args.db
result = dict()
try:
# bu.debug(str(replay_json))
if not wi.chk_JSON_replay(replay_json):
bu.debug("Invalid replay")
return None
2020-11-03 22:46:44 +02:00
2023-09-10 20:00:23 +03:00
result["_id"] = replay_json["_id"]
replay_summary = replay_json["data"]["summary"]
result["battle_start_timestamp"] = int(replay_summary["battle_start_timestamp"])
# TBD...
protagonist = int(replay_summary["protagonist"])
2023-09-10 20:00:23 +03:00
# For filtering replays per submitter
result["protagonist"] = protagonist
2018-10-08 00:14:56 +08:00
2023-09-10 20:00:23 +03:00
if account_id is None:
account_id = protagonist
elif protagonist != account_id:
if account_id in replay_summary["enemies"]:
# switch the teams...
if replay_summary["battle_result"] != 2:
if replay_summary["battle_result"] == 0:
replay_summary["battle_result"] = 1
else:
replay_summary["battle_result"] = 0
tmp = replay_summary["enemies"]
replay_summary["enemies"] = replay_summary["allies"]
replay_summary["allies"] = tmp
elif account_id in replay_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"]
2020-02-15 15:26:32 +02:00
2023-09-10 20:00:23 +03:00
for key in replay_summary_flds:
result[key] = replay_summary[key]
2023-09-10 20:00:23 +03:00
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_summary["details"]:
tank_id : int = player["vehicle_descr"]
btl_duration = max(btl_duration, player["time_alive"])
player_tank_tier = wg.get_tank_tier(tank_id)
btl_tier = max(btl_tier, player_tank_tier)
if player["dbid"] == account_id:
# player itself is not put in results['allies']
tmp = dict()
tmp["account_id"] = account_id
tmp["tank_id"] = tank_id
tmp["tank_tier"] = player_tank_tier
tmp["tank_name"] = wg.get_tank_name(tank_id)
tmp["tank_type"] = wg.get_tank_type(tank_id)
tmp["tank_nation"] = wg.get_tank_nation(tank_id)
tmp["is_premium"] = int(wg.is_premium(tank_id))
platoon_ndx = player["squad_index"]
tmp["squad_index"] = platoon_ndx
tmp["in_platoon"] = 0 if platoon_ndx is None else 1
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_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
result["battle_duration"] = btl_duration
## Rather use 'player' than incomprehensible 'protagonist'...
result["player"] = get_stat_id(
account_id, tank_id, result["battle_start_timestamp"]
)
bu.debug("Player stats_id: " + result["player"])
# remove platoon buddy from stats
if result["squad_index"] is not None:
for player in replay_summary["details"]:
bu.debug(str(player))
if (
(player["squad_index"] == result["squad_index"])
and (player["dbid"] in replay_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["alive"] = min(result["time_alive"] / result["battle_duration"], 1)
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
result["battle"] = (
str(REPLAY_I)
+ ": "
+ replay_summary["vehicle"]
+ " @"
+ replay_summary["map_name"]
)
bu.debug(str(result))
return result
except KeyError as err:
bu.warning("Invalid replay. Key not found", err)
except Exception as err:
bu.error(exception=err)
return None
2018-10-08 00:14:56 +08:00
def str2ints(stat_id_str: str) -> list:
2023-09-10 20:00:23 +03:00
"""Convert stat_id string e.g. 'account_id:tank_id' to list of ints"""
return [int(x) for x in stat_id_str.split(":")]
2020-02-13 00:21:36 +02:00
2023-09-10 20:00:23 +03:00
def prune_stat_id(stat_id_str: str) -> str:
stat_id = stat_id_str.split(":")
del stat_id[-1]
return ":".join(stat_id)
2021-04-17 14:34:05 +03:00
def get_stat_id(account_id: int, tank_id: int, battle_time: int) -> str:
2023-09-10 20:00:23 +03:00
return ":".join(map(str, [account_id, tank_id, battle_time]))
def get_stat_id_tank_tier(stat_id_str: str) -> str:
2023-09-10 20:00:23 +03:00
"""Return stat_id = account_id:tank_tier:battletime"""
global wg
try:
stat_id = str2ints(stat_id_str)
account_id = stat_id[0]
tank_tier = wg.get_tank_tier(stat_id[1])
battle_time = (stat_id[2] // BATTLE_TIME_BUCKET) * BATTLE_TIME_BUCKET
return get_stat_id(account_id, tank_tier, battle_time)
except Exception as err:
bu.error("Stats_id: " + stat_id_str, exception=err)
return None
def get_stat_id_tank(stat_id_str: str) -> str:
2023-09-10 20:00:23 +03:00
"""Return stat_id = account_id:tank_id:battletime"""
try:
stat_id = str2ints(stat_id_str)
account_id = stat_id[0]
tank_id = stat_id[1]
battle_time = (stat_id[2] // BATTLE_TIME_BUCKET) * BATTLE_TIME_BUCKET
return get_stat_id(account_id, tank_id, battle_time)
except Exception as err:
bu.error("Stats_id: " + stat_id_str, exception=err)
return None
def get_stat_id_player(stat_id_str: str) -> str:
2023-09-10 20:00:23 +03:00
"""Return stat_id = account_id:battletime"""
try:
stat_id = str2ints(stat_id_str)
battle_time = (stat_id[2] // BATTLE_TIME_BUCKET) * BATTLE_TIME_BUCKET
return ":".join(map(str, [stat_id[0], battle_time]))
except Exception as err:
bu.error("Stats_id: " + stat_id_str, exception=err)
return None
2020-11-03 22:46:44 +02:00
def get_account_id_f_stat_id(stat_id_str: str) -> int:
2023-09-10 20:00:23 +03:00
"""get account_id from a stat_id"""
try:
stat_id = str2ints(stat_id_str)
return stat_id[0]
except Exception as err:
bu.error("Stats_id: " + stat_id_str, exception=err)
return None
2018-10-08 00:14:56 +08:00
### main()
if __name__ == "__main__":
2023-09-10 20:00:23 +03:00
# To avoid 'Event loop is closed' RuntimeError due to compatibility issue with aiohttp
if sys.platform.startswith("win") and sys.version_info >= (3, 8):
try:
from asyncio import WindowsSelectorEventLoopPolicy
except ImportError:
pass
else:
if not isinstance(
asyncio.get_event_loop_policy(), WindowsSelectorEventLoopPolicy
):
asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy())
asyncio.run(main(sys.argv[1:]), debug=False)