Working stat estimates
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
*.venv
|
||||
*.venv
|
||||
temp.md
|
||||
216
main.py
216
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": {"<id>": {...}}}
|
||||
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
|
||||
# ==============================
|
||||
|
||||
Reference in New Issue
Block a user