Last active
November 19, 2025 04:57
-
-
Save escalonn/4218cb3ff341ee60f1dd054e16d6c00d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import json | |
| import re | |
| import sys | |
| import os | |
| from urllib import request | |
| from tabulate import tabulate | |
| API_URL = "https://api.yshelper.com/ys/getAbyssRank.php?star=all&role=all&lang=en" | |
| def fetch_data(timeout=10): | |
| with request.urlopen(API_URL, timeout=timeout) as r: | |
| return json.loads(r.read().decode("utf-8")) | |
| def read_owned_characters(path): | |
| with open(path, encoding="utf-8") as f: | |
| data = json.load(f) | |
| owned = {} | |
| for c in data["characters"]: | |
| good_name = c["key"] | |
| # Reconstruct human-readable name: insert a space before every uppercase letter (except at start) | |
| char_name = re.sub(r"(?<!^)(?=[A-Z])", " ", good_name) | |
| owned[char_name] = c["constellation"] | |
| return owned | |
| def extract_and_print(data, owned_chars): | |
| # total_samples = data["top_own"] | |
| # Build chars dict keyed by avatar | |
| misspellings = {"Ambor": "Amber"} | |
| extra_avatars = { | |
| "https://upload-bbs.mihoyo.com/game_record/genshin/character_icon/UI_AvatarIcon_PlayerGirl.png": "Traveler" | |
| } | |
| expected_missing_chars = { | |
| "Manekin", | |
| "Manekina", | |
| } | |
| chars = {} | |
| for group in data["result"][0]: | |
| for it in group["list"]: | |
| con_rates = [it["c0_rate"]] | |
| for i in range(1, 6): | |
| con_rates.append(con_rates[-1] + it[f"c{i}_rate"]) | |
| con_rates = [*(x / 100 for x in con_rates), 1] | |
| chars[it["avatar"]] = { | |
| "name": misspellings.get(it["name"], it["name"]), | |
| "con_rates": con_rates, | |
| } | |
| # validate owned chars' names vs yshelper data | |
| missing_chars = set(owned_chars) - {c["name"] for c in chars.values()} | |
| if missing_chars: | |
| print(f"YSHelper has no match for these owned characters: {', '.join(missing_chars)}") | |
| # assert missing_chars == expected_missing_chars | |
| for k, v in extra_avatars.items(): | |
| chars[k] = next(x for x in chars.values() if x["name"] == v) | |
| # Process teams | |
| teams = [] | |
| for team in data["result"][3]: | |
| appearances = team["use"] | |
| owners = team["has"] | |
| first_half = team["up_use_num"] | |
| second_half = team["down_use_num"] | |
| members = [] | |
| avatars = [] | |
| for c in team["role"]: | |
| avatars.append(c["avatar"]) | |
| members.append(chars[c["avatar"]]["name"]) | |
| if not all(c in owned_chars for c in members): | |
| continue | |
| first_half_score = first_half / owners | |
| second_half_score = second_half / owners | |
| # i'm not sure this is quite right, but i don't know what else to do. | |
| # the desire is that if the sample accounts love using e.g. wriothesley | |
| # but 99% of them have at least c1, then we should penalize wriothesley | |
| # teams when the user only has c0. | |
| # there is a side effect of unfairly penalizing teams with old characters | |
| # like sucrose that many people have at c6 but whose teams do not need | |
| # her c6. | |
| con_factor = 1 | |
| for char, avat in zip(members, avatars): | |
| con_factor *= chars[avat]["con_rates"][owned_chars[char]] | |
| first_half_adj_score = first_half_score * con_factor | |
| second_half_adj_score = second_half_score * con_factor | |
| team = { | |
| "appearances": appearances, | |
| "owners": owners, | |
| "first_half_uses": first_half, | |
| "second_half_uses": second_half, | |
| "members": members, | |
| "first_half_score": first_half_score, | |
| "first_half_adj_score": first_half_adj_score, | |
| "second_half_score": second_half_score, | |
| "second_half_adj_score": second_half_adj_score, | |
| } | |
| teams.append(team) | |
| # rank and print top 10 (scores as percentages) | |
| top_first = sorted(teams, key=lambda t: t["first_half_score"], reverse=True)[:10] | |
| top_second = sorted(teams, key=lambda t: t["second_half_score"], reverse=True)[:10] | |
| print() | |
| print(data["version"]) | |
| print(data["update"]) | |
| # Helper to build a table for a half ("first" or "second") | |
| def print_top_table(label: str, teams_list): | |
| rows = [ | |
| [t[f"{label}_half_score"], t[f"{label}_half_adj_score"], *t["members"]] | |
| for t in teams_list | |
| ] | |
| headers = ["Score", "Adj Score", "", "", "", ""] | |
| print(f"\nTop {label}-half teams:") | |
| print(tabulate(rows, headers, "presto", [".1%", ".3%"])) | |
| print_top_table("first", top_first) | |
| print_top_table("second", top_second) | |
| # Find a suitable database file in the current directory | |
| def find_database_file(): | |
| json_files = [f for f in os.listdir(".") if f.endswith(".json")] | |
| for json_file in json_files: | |
| try: | |
| owned_chars = read_owned_characters(json_file) | |
| if owned_chars: # Check if we found any characters | |
| return json_file, owned_chars | |
| except (json.JSONDecodeError, KeyError, FileNotFoundError): | |
| # Skip files that can't be parsed or don't have the expected structure | |
| continue | |
| return None, None | |
| def main(): | |
| # Check for command line argument | |
| if len(sys.argv) > 1: | |
| db_path = sys.argv[1] | |
| try: | |
| owned_chars = read_owned_characters(db_path) | |
| except Exception as e: | |
| print(f"Error reading specified file '{db_path}': {e}") | |
| return 1 | |
| else: | |
| # No argument provided, search for a suitable file | |
| db_path, owned_chars = find_database_file() | |
| if db_path is None: | |
| print("No suitable database file found in current directory") | |
| print("Please specify a path to your GOOD file (Genshin Optimizer format)") | |
| return 1 | |
| print(f'Using database file "{db_path}": {len(owned_chars)} owned characters') | |
| data = fetch_data() | |
| extract_and_print(data, owned_chars) | |
| return 0 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
this is outdated now