From 93a5da75eb085c78543aba5b083d4d738f1d108a Mon Sep 17 00:00:00 2001 From: jerick Date: Tue, 25 Nov 2025 14:22:36 -0500 Subject: [PATCH] Working stat estimates --- .gitignore | 3 +- main.py | 216 ++++++++++++++++++++--------------------------------- 2 files changed, 85 insertions(+), 134 deletions(-) diff --git a/.gitignore b/.gitignore index 266c348..8965cc5 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -*.venv \ No newline at end of file +*.venv +temp.md \ No newline at end of file diff --git a/main.py b/main.py index cb05f6d..6089558 100644 --- a/main.py +++ b/main.py @@ -12,6 +12,7 @@ TORN_API_KEY = "9VLK0Wte1BwXOheB" ENEMY_FACTION_ID = 52935 YOUR_FACTION_ID = 654321 ALLOWED_CHANNEL_ID = 1442876328536707316 +FFSCOUTER_KEY = "XYmWPO9ZYkLqnv3v" POLL_INTERVAL = 30 HIT_CHECK_INTERVAL = 60 @@ -47,72 +48,97 @@ class HitDispatchBot(commands.Bot): # Create bot now (ONE bot only) bot = HitDispatchBot(command_prefix="!", intents=intents) -# ------------------------------------------------------------------ -# Helper – get a single member's crimes & networth -# ------------------------------------------------------------------ -async def _get_user_stats(session: aiohttp.ClientSession, user_id: int): - url = f"https://api.torn.com/v2/user/{user_id}/personalstats?stat=criminaloffenses,networth&key={TORN_API_KEY}" +async def fetch_ffscouter_stats(session: aiohttp.ClientSession, torn_id: int): + """ + Calls FFScouter and returns predicted battle stats. + Uses an existing aiohttp session (caller must provide). + """ + url = f"https://ffscouter.com/api/v1/get-stats?key={FFSCOUTER_KEY}&targets={torn_id}" + + #print(url) + async with session.get(url) as resp: + if resp.status != 200: + print(f"FFScouter Error for {torn_id}:", resp.status) + return None + data = await resp.json() + # FFScouter v1 returns: {"code":200,"message":"OK","data": {"": {...}}} + if "data" not in data: + return None + inner = data["data"] + return inner.get(str(torn_id)) - crimes = 0 - networth_raw = 0 - for stat in data.get("personalstats", []): - if stat.get("name") == "criminaloffenses": - crimes = int(stat.get("value", 0)) - elif stat.get("name") == "networth": - networth_raw = float(stat.get("value", 0)) - - networth_mill = networth_raw / 1_000_000 - return crimes, networth_mill - -# ------------------------------------------------------------------ -# Updated fetch_enemy_faction – now pulls crimes & networth -# ------------------------------------------------------------------ async def fetch_enemy_faction(): """ - Pulls all active members from the target faction and enriches each - member with level, crimes and networth (in millions). + Pulls faction members from Torn (selections=members), + then fetches FFScouter stats in a single batch request. + Returns a list of enemies with name, id, level, and estimated BS. """ url = ( f"https://api.torn.com/v2/faction/{ENEMY_FACTION_ID}" - "?selections=members" - f"&key={TORN_API_KEY}" + f"?selections=members&key={TORN_API_KEY}" ) + enemies = [] + async with aiohttp.ClientSession() as session: + # --- Fetch faction members --- async with session.get(url) as resp: + if resp.status != 200: + print("Torn faction fetch error:", resp.status) + return enemies data = await resp.json() - # The response is a dict → {"members": [ {...}, ... ]} members_list = data.get("members", []) + if not members_list: + return enemies - enemies: list[dict] = [] + # --- Build comma-separated list of IDs --- + member_ids = [ + str(info.get("player_id", info.get("id", 0))) + for info in members_list + if info.get("status", {}).get("state", "Unknown") in ("Okay", "Idle") + ] + if not member_ids: + return enemies + + ids_str = ",".join(member_ids) + + # --- Single FFScouter batch request --- + ff_url = f"https://ffscouter.com/api/v1/get-stats?key={FFSCOUTER_KEY}&targets={ids_str}" + async with session.get(ff_url) as resp: + if resp.status != 200: + print("FFScouter batch error:", resp.status) + ff_data_list = [] + else: + ff_data_list = await resp.json() + + # --- Map FFScouter data by player_id for quick lookup --- + ff_map = {str(d["player_id"]): d for d in ff_data_list} + + # --- Build final enemies list --- for info in members_list: - status_state = info["status"]["state"] - if status_state not in ["Okay", "Idle"]: + pid = str(info.get("player_id", info.get("id", 0))) + state = info.get("status", {}).get("state", "Unknown") + if state not in ("Okay", "Idle"): continue + name = info.get("name", "Unknown") level = int(info.get("level", 0)) - - # ------------------------------------------------------------------ - # Pull the per‑user stats (crimes & networth) - # ------------------------------------------------------------------ - crimes, networth_mill = await _get_user_stats(session, info["id"]) + est = ff_map.get(pid, {}).get("bs_estimate_human", "?") enemies.append({ - "id": int(info["id"]), - "name": info["name"], - "status": status_state, - "level": level, - "crimes": crimes, - "networth": networth_mill, # millions + "id": int(pid), + "name": name, + "level": level, + "estimate": est }) - return enemies - + return enemies + async def update_enemy_queue_loop(): global enemy_queue @@ -124,86 +150,6 @@ async def update_enemy_queue_loop(): print(f"Enemy queue updated: {len(enemy_queue)} valid targets.") await asyncio.sleep(POLL_INTERVAL) -# ============================== -# STAT SYSTEM -# ============================== - -def estimate_battlestats(level: int, crimes: int, networth: float) -> str: - """ - Returns the score‑range string that matches the chart. - """ - - # ------------------------------------------------------------------ - # Bucket definitions (same as in the original code) - # ------------------------------------------------------------------ - level_buckets = [ - (6, "2k‑25k"), - (11, "20k‑25k"), - (26, "200k‑250k"), - (31, "2m‑2.5m"), - (50, "20m‑35m"), - (float('inf'), "200m‑250m") - ] - - crimes_buckets = [ - (5000, "2k‑25k"), - (10000, "20k‑25k"), - (20000, "200k‑250k"), - (30000, "2m‑2.5m"), - (50000, "20m‑35m"), - (float('inf'), "200m‑250m") - ] - - networth_buckets = [ - (50, "2k‑25k"), # < 50 m - (500, "20k‑25k"), # < 500 m - (5_000, "200k‑250k"), # < 5 b - (50_000, "2m‑2.5m"), # < 50 b - (200_000, "20m‑35m"), # < 200 b - (float('inf'), "200m‑250m") - ] - - def find_bucket(value: float, buckets): - for limit, label in buckets: - if value < limit: - return label - return "Unknown" - - lvl_label = find_bucket(level, level_buckets) - crime_label = find_bucket(crimes, crimes_buckets) - net_label = find_bucket(networth, networth_buckets) - - # ------------------------------------------------------------------ - # Convert a label like '200k‑250k' into its lower bound in points. - # We only need the *first* number – everything after the dash is - # ignored. The unit (k/m/b) tells us how to scale it. - # ------------------------------------------------------------------ - def score_value(label: str) -> float: - """ - Convert a label such as '200k‑250k' or '20m‑35' into the numeric - lower bound in points. - """ - m = re.match(r"(\d+(?:\.\d*)?)([kmb]?)", label, flags=re.IGNORECASE) - if not m: - return float('inf') # shouldn't happen - - number_str, unit = m.groups() - try: - num = float(number_str) - except ValueError: - return float('inf') - - unit = unit.lower() - if unit == 'm': - return num * 1_000_000 - if unit == 'k': - return num * 1_000 - # No unit → plain points - return num - - chosen_label = min([lvl_label, crime_label, net_label], key=score_value) - return chosen_label - # ============================== # ASSIGNMENT SYSTEM @@ -224,6 +170,7 @@ async def assign_next_target(): if not enemy_queue: return None, None + # Pop the weakest enemy (already sorted by score_value) enemy = enemy_queue.pop(0) attacker = get_next_attacker() @@ -238,7 +185,6 @@ async def assign_next_target(): return enemy, attacker - async def monitor_assignments_loop(): await bot.wait_until_ready() @@ -346,18 +292,22 @@ async def stats(ctx): await ctx.send("No active members found.") return - for e in enemies: - score_range = estimate_battlestats(e["level"], e["crimes"], e["networth"]) - # await ctx.send( - # f'{e["name"]} (ID:{e["id"]}) | ' - # f'Lv {e["level"]}, Crimes {e["crimes"]}, NetWorth ${e["networth"]:,.1f}m → ' - # f'Score: {score_range}' - # ) - print( - f'{e["name"]} (ID:{e["id"]}) | ' - f'Lv {e["level"]}, Crimes {e["crimes"]}, NetWorth ${e["networth"]:,.1f}m → ' - f'Score: {score_range}' - ) + lines = [ + f"**{e['name']}** (ID:{e['id']}) | Lv {e['level']} | Estimated BS: {e['estimate']}" + for e in enemies + ] + + # Discord chunking + chunk = "" + for line in lines: + if len(chunk) + len(line) > 1900: + await ctx.send(chunk) + chunk = "" + chunk += line + "\n" + if chunk: + await ctx.send(chunk) + + # ============================== # BOT READY # ==============================