From 2db5bdc87a547105cc9073f74cb83e3599592092 Mon Sep 17 00:00:00 2001 From: kmorrison Date: Fri, 13 Jan 2023 14:32:10 -0500 Subject: [PATCH] Stratz counterpicker --- couchdb.py | 29 +++-- counterpicker.py | 148 +++++++++++++++++++++++ fuzzy_hero_names.py | 45 +++++++ hero.py | 13 ++ matchlib.py | 2 + opendota.py | 44 +++++++ samplequery.gql | 284 ++++++++++++++++++++++++++++++++++++++++++++ stratz.py | 222 ++++++++++++++++++++++++++++++++++ talent_rater.py | 118 ++++++++++++++++++ 9 files changed, 895 insertions(+), 10 deletions(-) create mode 100644 counterpicker.py create mode 100644 fuzzy_hero_names.py create mode 100755 hero.py create mode 100644 samplequery.gql create mode 100644 stratz.py create mode 100644 talent_rater.py diff --git a/couchdb.py b/couchdb.py index 53be5f4..cfa45c2 100644 --- a/couchdb.py +++ b/couchdb.py @@ -52,30 +52,39 @@ def get_all_parsed_matches_more_recent_than( def get_all_matches_with_hero_after_start_time( - db: cloudant.database.CouchDatabase, start_time, hero_names=None + db: cloudant.database.CouchDatabase, start_time, hero_names=None, potential_hero_names=None ): if hero_names is None: hero_names = [] + if potential_hero_names is None: + potential_hero_names = [] hero_names = [name for name in hero_names if name] + potential_hero_names = [name for name in potential_hero_names if name] heroes = [opendota.find_hero(name) for name in hero_names] + potential_heroes = [opendota.find_hero(name) for name in potential_hero_names] query_dict = { "selector": { "start_time": {"$gt": start_time}, }, "sort": ["start_time"], } - if heroes: - if len(heroes) == 1: - query_dict["selector"]["players"] = { - "$elemMatch": {"hero_id": heroes[0]["id"]}, - } - else: - selector = [ - {"players": {"$elemMatch": {"hero_id": hero["id"]}}} for hero in heroes - ] + if len(heroes) + len(potential_heroes) == 1: + query_dict["selector"]["players"] = { + "$elemMatch": {"hero_id": heroes[0]["id"]}, + } + elif heroes or potential_heroes: + selector = [ + {"players": {"$elemMatch": {"hero_id": hero["id"]}}} for hero in heroes + ] + if selector: query_dict["selector"]["$and"] = selector + selector = [ + {"players": {"$elemMatch": {"hero_id": hero["id"]}}} for hero in potential_heroes + ] + if selector: + query_dict["selector"]["$or"] = selector query = db.get_query_result(**query_dict) return query diff --git a/counterpicker.py b/counterpicker.py new file mode 100644 index 0000000..f7be319 --- /dev/null +++ b/counterpicker.py @@ -0,0 +1,148 @@ +import argparse +import math +import itertools +import json +import collections +import fuzzy_hero_names +import tabulate + +import dateparser +import tabulate + +import couchdb +import matchlib +import pprint +import opendota +import stratz + +def team_of_interest(game, hero_ids, potential_hero_ids): + heroes_on_radiant = [player['player_slot'] < 127 for player in game['players'] if player['hero_id'] in hero_ids] + potential_heroes_on_radiant = [player['player_slot'] < 127 for player in game['players'] if player['hero_id'] in potential_hero_ids] + heroes_all_on_radiant = all(heroes_on_radiant) and any(potential_heroes_on_radiant) + heroes_all_on_dire = bool(not any(heroes_on_radiant)) and bool(not all(potential_heroes_on_radiant)) + return heroes_all_on_radiant, heroes_all_on_dire + + +def games_with_heroes_on_same_team(dbquery, hero_ids, potential_hero_ids): + for game in dbquery: + heroes_all_on_radiant, heroes_all_on_dire = team_of_interest(game, hero_ids, potential_hero_ids) + heroes_on_same_team = heroes_all_on_radiant or heroes_all_on_dire + if heroes_on_same_team: + yield game + +def calculate_winrate_for_opposing_heroes(game, heroes, potential_heroes): + heroes_all_on_radiant, heroes_all_on_dire = team_of_interest(game, heroes, potential_heroes) + wins = collections.Counter() + games = collections.Counter() + + if heroes_all_on_radiant: + dire_heroes = [player['hero_id'] for player in game['players'] if player['player_slot'] >= 127] + wins.update([hero_id for hero_id in dire_heroes if not game['radiant_win']]) + games.update(dire_heroes) + if heroes_all_on_dire: + radiant_heroes = [player['hero_id'] for player in game['players'] if player['player_slot'] < 127] + wins.update([hero_id for hero_id in radiant_heroes if game['radiant_win']]) + games.update(radiant_heroes) + + return wins, games + +def calculate_winrates_from_localdb(dbquery, hero_ids, potential_hero_ids): + wins = collections.Counter() + games = collections.Counter() + for i, game in enumerate(games_with_heroes_on_same_team(dbquery, hero_ids, potential_hero_ids)): + hero_wins, hero_games = calculate_winrate_for_opposing_heroes(game, hero_ids, potential_hero_ids) + wins += hero_wins + games += hero_games + print(f'{i} games analyzed') + + hero_winrate_table = [] + for hero_id, game_count in games.items(): + winrate = wins[hero_id] / game_count + hero = opendota.find_hero_by_id(hero_id) + standard_deviation = math.sqrt( + winrate * (1 - winrate) / game_count + ) + hero_winrate_table.append([ + hero['localized_name'], + wins['hero_id'], + game_count, + winrate, + standard_deviation, + ]) + + hero_winrate_table.sort(key=lambda row: row[3], reverse=True) + print(tabulate.tabulate(hero_winrate_table, headers=['Hero', 'Wins', 'Games', 'Winrate', 'Standard deviation'])) + +def calculate_from_opendota(hero_ids): + hero_matchups = [opendota.get_matchups(hero_id) for hero_id in hero_ids] + wins = {} + games = {} + for matchup_payload in hero_matchups: + for hero_matchup in matchup_payload: + wins.setdefault(hero_matchup['hero_id'], 0) + wins[hero_matchup['hero_id']] += hero_matchup['wins'] + games.setdefault(hero_matchup['hero_id'], 0) + games[hero_matchup['hero_id']] += hero_matchup['games_played'] + + hero_winrate_table = [] + for hero_id, game_count in games.items(): + hero_winrate_table.append([ + opendota.find_hero_by_id(hero_id)['localized_name'], + wins[hero_id], + game_count, + wins[hero_id] / game_count, + ]) + hero_winrate_table.sort(key=lambda row: row[3], reverse=True) + print(tabulate.tabulate(hero_winrate_table, headers=['Hero', 'Wins', 'Games', 'Winrate'])) + +def suggest_counterpicks(heroes): + with_mtx, vs_mtx = stratz.load_fixed_matchups() + counterpick_score = {} + for hero in heroes: + counterpicks = vs_mtx[hero['id']] + for counterpick in counterpicks.values(): + counterpick_score.setdefault(counterpick['heroId2'], {}) + counterpick_score[counterpick['heroId2']][hero['id']] = counterpick['synergy'] + counterpick_score[counterpick['heroId2']].setdefault('total', 0) + counterpick_score[counterpick['heroId2']]['total'] += counterpick['synergy'] + counterpick_score = sorted(counterpick_score.items(), key=lambda x: x[1]['total']) + return counterpick_score[:20], counterpick_score[-20:] + +def counterpick_report(hero_names): + heroes = [fuzzy_hero_names.match(hero_name.strip()) for hero_name in hero_names] + counterpicks, bad_picks = suggest_counterpicks(heroes) + counterpick_table = [] + for pick in counterpicks: + synergy_by_hero = [pick[1].get(hero['id']) for hero in heroes] + counterpick_table.append([ + opendota.find_hero_by_id(pick[0])['localized_name'], + pick[1]['total'], + *synergy_by_hero + ]) + print(tabulate.tabulate(counterpick_table, headers=['Hero', 'Total synergy'] + [hero['localized_name'] for hero in heroes])) + + badpick_table = [] + for pick in bad_picks: + synergy_by_hero = [pick[1].get(hero['id'], None) for hero in heroes] + badpick_table.append([ + opendota.find_hero_by_id(pick[0])['localized_name'], + pick[1]['total'], + *synergy_by_hero + ]) + print(tabulate.tabulate(badpick_table, headers=['Hero', 'Total synergy'] + [hero['localized_name'] for hero in heroes])) + + +if __name__ == "__main__": + import sys + # parser = argparse.ArgumentParser() + + # parser.add_argument("--process-queue", action="store_true") + # parser.add_argument("--populate-queue", action="store_true") + # parser.add_argument("--max-matches-to-queue", type=int, default=5) + + # args = parser.parse_args() + # if args.process_queue: + # pass + hero_names = sys.argv[1:] + counterpick_report(hero_names) + \ No newline at end of file diff --git a/fuzzy_hero_names.py b/fuzzy_hero_names.py new file mode 100644 index 0000000..4225acf --- /dev/null +++ b/fuzzy_hero_names.py @@ -0,0 +1,45 @@ +import opendota + +def _score_hero_name(hero_name, input): + normalized_hero_name = hero_name.lower() + normalized_input = input.strip().lower() + if normalized_input == normalized_hero_name: + return 100 + if normalized_hero_name.startswith(normalized_input): + return 10 * len(normalized_input) + + # Try to do acronym matching, ie. AM -> Anti-Mage + parts = normalized_hero_name.split() + if len(parts) == 2 and len(normalized_input) == 2: + return 25 * (normalized_input[0] == parts[0][0]) + 25 * (normalized_input[1] == parts[1][0]) + if len(parts) == 2 and len(normalized_input) > 3 and parts[1].startswith(normalized_input): + return 5 * len(normalized_input) + + parts = normalized_hero_name.split('-') + if len(parts) == 2 and len(normalized_input) == 2: + return 25 * (normalized_input[0] == parts[0][0]) + 25 * (normalized_input[1] == parts[1][0]) + if len(parts) == 2 and len(normalized_input) > 3 and parts[1].startswith(normalized_input): + return 5 * len(normalized_input) + + return 0 + +def match(input): + heroes = opendota.load_hero_list().values() + best_score = 0 + best_match = None + for hero in heroes: + score = _score_hero_name(hero['localized_name'], input) + if score > best_score: + best_score = score + best_match = hero + return best_match + +if __name__ == '__main__': + print(match('AM')) + print(match('grim')) + print(match('jugg')) + print(match('CM')) + print(match('maiden')) + print(match('primal')) + print(match('OD')) + print(match('Pango')) \ No newline at end of file diff --git a/hero.py b/hero.py new file mode 100755 index 0000000..a7211f6 --- /dev/null +++ b/hero.py @@ -0,0 +1,13 @@ +#!python +import opendota +import fuzzy_hero_names +import sys + +if __name__ == '__main__': + input = sys.argv[1] + + try: + input = int(input) + print(opendota.find_hero_by_id(input)) + except ValueError: + print(fuzzy_hero_names.match(input)) \ No newline at end of file diff --git a/matchlib.py b/matchlib.py index 69db436..d274ef8 100644 --- a/matchlib.py +++ b/matchlib.py @@ -163,6 +163,8 @@ def iterate_matches(date_string, limit=200, page_size=DEFAULT_QUERY_PAGE_SIZE): def is_fully_parsed(match): + if "players" not in match: + return False return bool(match["players"][0].get("purchase_log", None)) diff --git a/opendota.py b/opendota.py index 20a8f7a..919723d 100644 --- a/opendota.py +++ b/opendota.py @@ -31,6 +31,16 @@ def request_parse(match_id): ) return response.json() +def check_job(job_id): + response = requests.get( + f"{API_ROOT}/request/{job_id}", + params=dict(api_key=secret.OPENDOTA_API_KEY), + ) + return response + +def all_heroes(): + return load_hero_list().values() + def find_hero(heroname): for hero in load_hero_list().values(): @@ -44,6 +54,10 @@ def find_hero_by_id(hero_id): return load_hero_list().get(str(hero_id)) +def find_hero_name_by_id(hero_id): + return load_hero_list().get(str(hero_id))["localized_name"] + + def get_hero_id(heroname): return find_hero(heroname)["id"] @@ -84,6 +98,36 @@ def get_heroes_table(): ) return response.json() +@opendota_retry +def get_abilities(): + response = requests.get( + f"{API_ROOT}/constants/abilities", + params=dict( + api_key=secret.OPENDOTA_API_KEY, + ), + ) + return response.json() + +@opendota_retry +def get_ability_ids(): + response = requests.get( + f"{API_ROOT}/constants/ability_ids", + params=dict( + api_key=secret.OPENDOTA_API_KEY, + ), + ) + return response.json() + +@opendota_retry +def get_matchups(hero_id): + response = requests.get( + f"{API_ROOT}/heroes/{hero_id}/matchups", + params=dict( + api_key=secret.OPENDOTA_API_KEY, + ), + ) + return response.json() + @opendota_retry def query_explorer(query): diff --git a/samplequery.gql b/samplequery.gql new file mode 100644 index 0000000..38be662 --- /dev/null +++ b/samplequery.gql @@ -0,0 +1,284 @@ +query { + heroStats{ + + hero1: matchUp(heroId: 1, take: 137){ + with { + heroId1 + heroId2 + week + bracketBasicIds + matchCount + count + synergy + winRateHeroId1 + winRateHeroId2 + winsAverage + }, + vs { + heroId1 + heroId2 + week + bracketBasicIds + matchCount + count + synergy + winRateHeroId1 + winRateHeroId2 + winsAverage + }, + }, + + + hero2: matchUp(heroId: 2, take: 137){ + with { + heroId1 + heroId2 + week + bracketBasicIds + matchCount + count + synergy + winRateHeroId1 + winRateHeroId2 + winsAverage + }, + vs { + heroId1 + heroId2 + week + bracketBasicIds + matchCount + count + synergy + winRateHeroId1 + winRateHeroId2 + winsAverage + }, + }, + + + hero3: matchUp(heroId: 3, take: 137){ + with { + heroId1 + heroId2 + week + bracketBasicIds + matchCount + count + synergy + winRateHeroId1 + winRateHeroId2 + winsAverage + }, + vs { + heroId1 + heroId2 + week + bracketBasicIds + matchCount + count + synergy + winRateHeroId1 + winRateHeroId2 + winsAverage + }, + }, + + + hero4: matchUp(heroId: 4, take: 137){ + with { + heroId1 + heroId2 + week + bracketBasicIds + matchCount + count + synergy + winRateHeroId1 + winRateHeroId2 + winsAverage + }, + vs { + heroId1 + heroId2 + week + bracketBasicIds + matchCount + count + synergy + winRateHeroId1 + winRateHeroId2 + winsAverage + }, + }, + + + hero5: matchUp(heroId: 5, take: 137){ + with { + heroId1 + heroId2 + week + bracketBasicIds + matchCount + count + synergy + winRateHeroId1 + winRateHeroId2 + winsAverage + }, + vs { + heroId1 + heroId2 + week + bracketBasicIds + matchCount + count + synergy + winRateHeroId1 + winRateHeroId2 + winsAverage + }, + }, + + + hero6: matchUp(heroId: 6, take: 137){ + with { + heroId1 + heroId2 + week + bracketBasicIds + matchCount + count + synergy + winRateHeroId1 + winRateHeroId2 + winsAverage + }, + vs { + heroId1 + heroId2 + week + bracketBasicIds + matchCount + count + synergy + winRateHeroId1 + winRateHeroId2 + winsAverage + }, + }, + + + hero7: matchUp(heroId: 7, take: 137){ + with { + heroId1 + heroId2 + week + bracketBasicIds + matchCount + count + synergy + winRateHeroId1 + winRateHeroId2 + winsAverage + }, + vs { + heroId1 + heroId2 + week + bracketBasicIds + matchCount + count + synergy + winRateHeroId1 + winRateHeroId2 + winsAverage + }, + }, + + + hero8: matchUp(heroId: 8, take: 137){ + with { + heroId1 + heroId2 + week + bracketBasicIds + matchCount + count + synergy + winRateHeroId1 + winRateHeroId2 + winsAverage + }, + vs { + heroId1 + heroId2 + week + bracketBasicIds + matchCount + count + synergy + winRateHeroId1 + winRateHeroId2 + winsAverage + }, + }, + + + hero9: matchUp(heroId: 9, take: 137){ + with { + heroId1 + heroId2 + week + bracketBasicIds + matchCount + count + synergy + winRateHeroId1 + winRateHeroId2 + winsAverage + }, + vs { + heroId1 + heroId2 + week + bracketBasicIds + matchCount + count + synergy + winRateHeroId1 + winRateHeroId2 + winsAverage + }, + }, + + + hero10: matchUp(heroId: 10, take: 137){ + with { + heroId1 + heroId2 + week + bracketBasicIds + matchCount + count + synergy + winRateHeroId1 + winRateHeroId2 + winsAverage + }, + vs { + heroId1 + heroId2 + week + bracketBasicIds + matchCount + count + synergy + winRateHeroId1 + winRateHeroId2 + winsAverage + }, + }, + + } +} \ No newline at end of file diff --git a/stratz.py b/stratz.py new file mode 100644 index 0000000..86dc8bd --- /dev/null +++ b/stratz.py @@ -0,0 +1,222 @@ +import secret +import json +import copy +import math +import requests +import itertools +import opendota +import time +import more_itertools +import pickle + +url = "https://api.stratz.com/graphql" +hero_stats_query = """ {}: matchUp(heroId: {}, take: 137, matchLimit: 200){{ + with {{ + heroId1 + heroId2 + week + bracketBasicIds + matchCount + synergy + winCount + winRateHeroId1 + winRateHeroId2 + winsAverage + }}, + vs {{ + heroId1 + heroId2 + week + bracketBasicIds + matchCount + synergy + winCount + winRateHeroId1 + winRateHeroId2 + winsAverage + }}, + }},""" + +query = """ +query {{ + heroStats{{ + {} + }} +}} +""" + +HERO_MATHCUPS_FILENAME = "hero_matchups.json" + +def do_query(query): + headers = {"Authorization": f"Bearer {secret.STRATZ_API_KEY}"} + resp = requests.post(url, json={'query': query}, headers=headers) + return resp + +def build_query(heroes): + hero_query_strings = [hero_stats_query.format("hero" + str(hero["id"]), str(hero["id"])) for hero in heroes] + all_heroes_query = '\n'.join(hero_query_strings) + return query.format(all_heroes_query) + +def build_hero_matchups(): + heroes = opendota.load_hero_list().values() + matchup_by_heroes = {} + + for some_heroes in more_itertools.chunked(heroes, 20): + query = build_query(some_heroes) + resp = do_query(query) + if resp.status_code != 200: + raise Exception("Query failed to run by returning code of {}".format(resp.status_code)) + + response_body = resp.json() + + matchup_by_heroes.update(response_body["data"]["heroStats"]) + + time.sleep(0.1) # Be nice to stratz API, rate limiting is a little aggressive + + + return matchup_by_heroes + +def save_hero_matchups(filename=HERO_MATHCUPS_FILENAME): + response_body = build_hero_matchups() + with open(filename, 'w') as f: + f.write(json.dumps(response_body)) + +def load_hero_matchups(filename=HERO_MATHCUPS_FILENAME): + with open(filename, 'r') as f: + return json.loads(f.read()) + +def make_and_save_fixed_matchups(): + with_mtx, vs_mtx = fix_winrates(*make_with_vs_matrix()) + + with open('fixed_hero_matchups.pkl', 'wb') as f: + pickle.dump({'with': with_mtx, 'vs': vs_mtx}, f) + +def load_fixed_matchups(filename='fixed_hero_matchups.pkl'): + with open(filename, 'rb') as f: + data = pickle.load(f) + return data['with'], data['vs'] + + +def make_with_vs_matrix(): + raw_query_data = load_hero_matchups() + ally_matrix = {} + enemy_matrix = {} + for hero in opendota.load_hero_list().values(): + hero_id = hero['id'] + + specific_ally_matrix = raw_query_data[f'hero{hero_id}'][0]['with'] + for hero_pair_info in specific_ally_matrix: + ally_matrix.setdefault(hero_id, {}) + ally_matrix[hero_id][hero_pair_info['heroId2']] = hero_pair_info + + specific_enemy_matrix = raw_query_data[f'hero{hero_id}'][0]['vs'] + for hero_pair_info in specific_enemy_matrix: + enemy_matrix.setdefault(hero_id, {}) + enemy_matrix[hero_id][hero_pair_info['heroId2']] = hero_pair_info + + return ally_matrix, enemy_matrix + +def check_mtx(mtx, hero_ids, flip_synergy=False): + missing_one_way = [] + missing_two_way = [] + inconsistent = [] + multiplier = 1 + if flip_synergy: + multiplier = -1 + for (id1, id2) in itertools.combinations(hero_ids, 2): + with_row = mtx[id1] + with2_row = mtx[id2] + if id2 not in with_row and id1 not in with2_row: + missing_two_way.append((id1, id2)) + elif id2 not in with_row: + missing_one_way.append((id1, id2)) + elif id1 not in with2_row: + missing_one_way.append((id2, id1)) + elif with_row[id2]['synergy'] != (multiplier * with2_row[id1]['synergy']): + inconsistent.append((id1, id2)) + return missing_one_way, missing_two_way, inconsistent + +def check_winrate_matrix_integrity(with_mtx, vs_mtx): + hero_ids = set([h['id'] for h in opendota.load_hero_list().values()]) + with_one_way, with_two_way, with_inconsistent = check_mtx(with_mtx, hero_ids) + vs_one_way, vs_two_way, vs_inconsistent = check_mtx(vs_mtx, hero_ids, flip_synergy=True) + print(f'Total heroes {len(hero_ids) ** 2}') + print('Missing with_mtx one way', len(with_one_way)) + print('Missing with_mtx two way', len(with_two_way)) + print('Inconsistent with_mtx pairs', len(with_inconsistent)) + print('Missing vs_mtx one way', len(vs_one_way)) + print('Missing vs_mtx two way', len(vs_two_way)) + print('Inconsistent vs_mtx pairs', len(vs_inconsistent)) + +def synergy(winrate1, winrate2, observed_winrate): + return round((observed_winrate - (-.48 + (.98 * winrate1) + (.98 * winrate2))) * 100, 3) + +def counters(winrate1, winrate2, observed_winrate): + return round((observed_winrate - (.5 + winrate1 - winrate2)) * 100, 3) + +def fix_mtx(with_mtx, hero_winrates, synergy_func): + new_mtx = {} + for hero in opendota.all_heroes(): + with_row = with_mtx[hero['id']] + winrate = hero_winrates[hero['id']] + hero_id = int(hero['id']) + new_mtx.setdefault(hero_id, {}) + for other_hero in with_row.values(): + other_hero_id = int(other_hero['heroId2']) + other_hero_winrate = hero_winrates[other_hero_id] + synergy = synergy_func(winrate, other_hero_winrate, other_hero['winsAverage']) + new_mtx[hero_id][other_hero_id] = copy.deepcopy(other_hero) + new_mtx[hero_id][other_hero_id]['synergy'] = synergy + new_mtx[hero_id][other_hero_id]['winRateHeroId1'] = winrate + new_mtx[hero_id][other_hero_id]['winRateHeroId2'] = other_hero_winrate + + return new_mtx + + +def fix_winrates(with_mtx, vs_mtx): + hero_winrates = {} + for hero in opendota.all_heroes(): + with_row = with_mtx[hero['id']] + matches = sum([row['matchCount'] for row in with_row.values()]) + wins = sum([row['winCount'] for row in with_row.values()]) + overall_winrate = wins / matches + hero_winrates[hero['id']] = overall_winrate + + fixed_with_mtx = fix_mtx(with_mtx, hero_winrates, synergy) + fixed_vs_mtx = fix_mtx(vs_mtx, hero_winrates, counters) + return fixed_with_mtx, fixed_vs_mtx + + +def check_hero(hero_id, with_mtx, vs_mtx): + vs_row = vs_mtx[hero_id] + matches = sum([row['matchCount'] for row in vs_row.values()]) + wins = sum([row['winCount'] for row in vs_row.values()]) + overall_winrate = wins / matches + reported_winrates = [row['winRateHeroId1'] for row in vs_row.values()] + assert len(list(set(reported_winrates))) == 1 + print(overall_winrate, reported_winrates[0]) + for other_hero in vs_row.values(): + observed_synergy = counters( + reported_winrates[0], + other_hero['winRateHeroId2'], + other_hero['winCount'] / other_hero['matchCount'], + ) + print( + other_hero['heroId2'], + other_hero['synergy'], + observed_synergy, + other_hero['synergy'] - observed_synergy, + ) + + +if __name__ == '__main__': + print(f'saving hero matchups to {HERO_MATHCUPS_FILENAME}') + #save_hero_matchups(HERO_MATHCUPS_FILENAME) + print(f'saved hero matchups to {HERO_MATHCUPS_FILENAME}') + + with_mtx, vs_mtx = make_with_vs_matrix() + #check_winrate_matrix_integrity(with_mtx, vs_mtx) + #make_and_save_fixed_matchups() + with_mtx, vs_mtx = load_fixed_matchups() + check_winrate_matrix_integrity(with_mtx, vs_mtx) + #check_hero(61, with_mtx, vs_mtx) \ No newline at end of file diff --git a/talent_rater.py b/talent_rater.py new file mode 100644 index 0000000..50361b6 --- /dev/null +++ b/talent_rater.py @@ -0,0 +1,118 @@ +import argparse +import math +import json + +import dateparser +import tabulate + +import couchdb +import matchlib +import opendota + +def fetch_and_store_ability_constants(): + # TODO: Probably should generalize this to refresh all local constants + abilities = opendota.get_abilities() + with open('abilities.json', 'w') as f: + f.write(json.dumps(abilities)) + + ability_ids = opendota.get_ability_ids() + with open('ability_ids.json', 'w') as f: + f.write(json.dumps(ability_ids)) + +def load_ability_constants(): + with open('abilities.json') as f: + abilities = json.loads(f.read()) + + with open('ability_ids.json') as f: + ability_ids = json.loads(f.read()) + return abilities, ability_ids + + +def extract_ability_upgrades_from_player_data(player_data): + hero = opendota.find_hero_by_id(player_data["hero_id"]) + + ability_ids = player_data["ability_upgrades_arr"] + + is_radiant = bool(player_data["player_slot"] < 127) + radiant_win = bool(player_data["radiant_win"]) + other_team_won = bool(is_radiant ^ radiant_win) + + return dict( + hero=hero, + ability_ids=ability_ids, + is_radiant=is_radiant, + player_won=not (other_team_won), + ) + +# XXX +def _is_talent(ability_info): + return not bool(ability_info.get('behavior', None)) + +def calculate_talent_winrates(dbquery, hero_name, max_level=16): + abilities, ability_ids = load_ability_constants() + ability_wins_and_games = {} + hero = opendota.find_hero(hero_name) + all_games = 0 + for game in dbquery: + for player_game in game['players']: + if player_game['hero_id'] != hero['id']: + continue + ability_data = extract_ability_upgrades_from_player_data(player_game) + if ability_data['ability_ids'] is None: + continue + all_games += 1 + for i, ability_id in enumerate(ability_data['ability_ids'][:max_level]): + ability_name = ability_ids.get(str(ability_id), None) + if ability_name is None: + continue + + full_ability_info = abilities[ability_name] + if not full_ability_info: + continue + if not _is_talent(full_ability_info): + continue + + ability_wins_and_games.setdefault(full_ability_info['dname'], {}) + ability_wins_and_games[full_ability_info['dname']].setdefault('earliest_taken', 30) + if ability_wins_and_games[full_ability_info['dname']]['earliest_taken'] > i + 1: + ability_wins_and_games[full_ability_info['dname']]['earliest_taken'] = i + 1 + + ability_wins_and_games[full_ability_info['dname']].setdefault('wins', 0) + ability_wins_and_games[full_ability_info['dname']]['wins'] += ability_data['player_won'] + ability_wins_and_games[full_ability_info['dname']].setdefault('games', 0) + ability_wins_and_games[full_ability_info['dname']]['games'] += 1 + + print(11111111, all_games) + return ability_wins_and_games + + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--hero", type=str, default="") + parser.add_argument("--start-time", type=str, default='Jan 1 2020') + parser.add_argument("--max-level", type=int, default=16) + + args = parser.parse_args() + + db = couchdb.get_matches_db() + start_time = dateparser.parse(args.start_time).timestamp() + dbquery = couchdb.get_all_matches_with_hero_after_start_time( + db, + start_time, + [args.hero], + ) + talent_winrates = calculate_talent_winrates(dbquery, args.hero, args.max_level) + talent_winrate_table = [] + for talent_name, game_info in talent_winrates.items(): + winrate = game_info['wins'] / game_info['games'] + standard_deviation = math.sqrt( + winrate * (1 - winrate) / game_info.get("games", 1) + ) + + talent_winrate_table.append([talent_name, game_info['earliest_taken'], game_info['wins'], game_info['games'], winrate, standard_deviation]) + talent_winrate_table = sorted(talent_winrate_table, key=lambda x: x[1]) + print( + tabulate.tabulate(talent_winrate_table, headers=["Name", "Level", "Wins", "Games", "Winrate", "StdDev"]) + ) + print("\n") \ No newline at end of file