Skip to content

Instantly share code, notes, and snippets.

@escalonn
Last active November 19, 2025 04:57
Show Gist options
  • Select an option

  • Save escalonn/4218cb3ff341ee60f1dd054e16d6c00d to your computer and use it in GitHub Desktop.

Select an option

Save escalonn/4218cb3ff341ee60f1dd054e16d6c00d to your computer and use it in GitHub Desktop.
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())
@escalonn
Copy link
Author

this is outdated now

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment