Rookie system WIP: added seasons_played sync, career stat storage, and leaderboard fixes
This commit is contained in:
parent
2ff013da3b
commit
acfc7d48a0
8 changed files with 3953 additions and 1114 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -3,52 +3,91 @@ import os
|
||||||
|
|
||||||
PIR_PATH = os.path.join(os.path.dirname(__file__), "player_identity_registry.json")
|
PIR_PATH = os.path.join(os.path.dirname(__file__), "player_identity_registry.json")
|
||||||
|
|
||||||
|
|
||||||
class PlayerIdentityRegistry:
|
class PlayerIdentityRegistry:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
if os.path.exists(PIR_PATH):
|
if os.path.exists(PIR_PATH):
|
||||||
with open(PIR_PATH, "r", encoding="utf-8") as f:
|
with open(PIR_PATH, "r", encoding="utf-8") as f:
|
||||||
self.data = json.load(f)
|
self.data = json.load(f)
|
||||||
else:
|
else:
|
||||||
self.data = {"uuid_to_canonical": {}, "canonical": {}}
|
self.data = {
|
||||||
|
"uuid_to_canonical": {},
|
||||||
|
"canonical": {}
|
||||||
|
}
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
with open(PIR_PATH, "w", encoding="utf-8") as f:
|
with open(PIR_PATH, "w", encoding="utf-8") as f:
|
||||||
json.dump(self.data, f, indent=2)
|
json.dump(self.data, f, indent=2)
|
||||||
|
|
||||||
def resolve(self, uuid):
|
# ---------------------------------------------------------
|
||||||
"""Return canonical ID for a UUID, or create one."""
|
# Resolve UUID → canonical ID (and create if needed)
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
def resolve(self, uuid, season=None):
|
||||||
uuid = str(uuid)
|
uuid = str(uuid)
|
||||||
|
|
||||||
|
# Known UUID
|
||||||
if uuid in self.data["uuid_to_canonical"]:
|
if uuid in self.data["uuid_to_canonical"]:
|
||||||
return self.data["uuid_to_canonical"][uuid]
|
cid = self.data["uuid_to_canonical"][uuid]
|
||||||
|
entry = self.data["canonical"][cid]
|
||||||
|
if season is not None:
|
||||||
|
entry["last_season"] = season
|
||||||
|
return cid
|
||||||
|
|
||||||
# Create new canonical ID
|
# New UUID → new canonical ID
|
||||||
canonical = f"player_{len(self.data['canonical'])+1}"
|
cid = f"player_{len(self.data['canonical']) + 1}"
|
||||||
self.data["uuid_to_canonical"][uuid] = canonical
|
self.data["uuid_to_canonical"][uuid] = cid
|
||||||
self.data["canonical"][canonical] = {"uuids": [uuid], "names": []}
|
self.data["canonical"][cid] = {
|
||||||
|
"uuids": [uuid],
|
||||||
|
"names": [],
|
||||||
|
"team_history": [],
|
||||||
|
"first_season": season,
|
||||||
|
"last_season": season,
|
||||||
|
}
|
||||||
self.save()
|
self.save()
|
||||||
return canonical
|
return cid
|
||||||
|
|
||||||
def add_name(self, canonical, name):
|
# ---------------------------------------------------------
|
||||||
entry = self.data["canonical"].setdefault(canonical, {"uuids": [], "names": []})
|
# Track name changes
|
||||||
if name not in entry["names"]:
|
# ---------------------------------------------------------
|
||||||
|
def update_name(self, canonical_id, name):
|
||||||
|
entry = self.data["canonical"].get(canonical_id)
|
||||||
|
if not entry:
|
||||||
|
return
|
||||||
|
|
||||||
|
if name and name not in entry["names"]:
|
||||||
entry["names"].append(name)
|
entry["names"].append(name)
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def merge(self, uuid_a, uuid_b):
|
# ---------------------------------------------------------
|
||||||
"""Merge two UUIDs into one canonical identity."""
|
# Track team history
|
||||||
ca = self.resolve(uuid_a)
|
# ---------------------------------------------------------
|
||||||
cb = self.resolve(uuid_b)
|
def update_team(self, canonical_id, team):
|
||||||
if ca == cb:
|
entry = self.data["canonical"].get(canonical_id)
|
||||||
return ca
|
if not entry:
|
||||||
|
return
|
||||||
|
|
||||||
# Merge cb into ca
|
if team and (not entry["team_history"] or entry["team_history"][-1] != team):
|
||||||
self.data["canonical"][ca]["uuids"].extend(self.data["canonical"][cb]["uuids"])
|
entry["team_history"].append(team)
|
||||||
self.data["canonical"][ca]["names"].extend(self.data["canonical"][cb]["names"])
|
self.save()
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Backwards compatibility: old method name
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
def add_name(self, canonical_id, name):
|
||||||
|
"""Legacy alias for update_name()."""
|
||||||
|
self.update_name(canonical_id, name)
|
||||||
|
|
||||||
# Update uuid_to_canonical
|
def add_team(self, canonical_id, team):
|
||||||
for u in self.data["canonical"][cb]["uuids"]:
|
"""Legacy alias for update_team()."""
|
||||||
self.data["uuid_to_canonical"][u] = ca
|
self.update_team(canonical_id, team)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
del self.data["canonical"][cb]
|
# ---------------------------------------------------------
|
||||||
self.save()
|
# Rookie detection
|
||||||
return ca
|
# ---------------------------------------------------------
|
||||||
|
def get_first_season(self, canonical_id):
|
||||||
|
entry = self.data["canonical"].get(canonical_id)
|
||||||
|
if not entry:
|
||||||
|
return None
|
||||||
|
return entry.get("first_season")
|
||||||
|
|
@ -1,24 +1,98 @@
|
||||||
def compute_rookie_leaderboard(career_db, tag="slayer", limit=10):
|
import os
|
||||||
rookies = []
|
import json
|
||||||
|
|
||||||
for cid, pdata in career_db["players"].items():
|
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
maps = pdata["career"].get("maps", 0)
|
CAREER_DB_PATH = os.path.join(PROJECT_ROOT, "career_stats.json")
|
||||||
if maps >= 10:
|
|
||||||
continue # not a rookie
|
|
||||||
|
|
||||||
tag_data = pdata.get(tag, {})
|
|
||||||
rookies.append({
|
# ---------------------------------------------------------
|
||||||
"canonical_id": cid,
|
# Load DB
|
||||||
"name": pdata["name"],
|
# ---------------------------------------------------------
|
||||||
"team": pdata.get("team", ""),
|
|
||||||
"maps": maps,
|
def load_career_db():
|
||||||
"kd": pdata["career"]["KD"],
|
if not os.path.exists(CAREER_DB_PATH):
|
||||||
"kills": pdata["career"]["kills"],
|
return None
|
||||||
"score": pdata["career"]["damage"], # or actual score if available
|
with open(CAREER_DB_PATH, "r", encoding="utf-8") as f:
|
||||||
"tag_raw": tag_data.get("raw", 0.0),
|
return json.load(f)
|
||||||
"tag_pct": tag_data.get("pct", 0.0),
|
|
||||||
"tag_tier": tag_data.get("tier", "D"),
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Identify rookies
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
def get_rookies(db, season):
|
||||||
|
rookies = {}
|
||||||
|
|
||||||
|
for pid, p in db.get("players", {}).items():
|
||||||
|
seasons_played = p.get("seasons_played") or ""
|
||||||
|
season_list = [int(s) for s in seasons_played.split(",") if s.isdigit()]
|
||||||
|
|
||||||
|
# True rookie = only played this season
|
||||||
|
if len(season_list) == 1 and season_list[0] == season:
|
||||||
|
rookies[pid] = p
|
||||||
|
|
||||||
|
return rookies
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Rank rookies by a selected tag
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
def rank_rookies_by_tag(season, tag_name, limit=10):
|
||||||
|
"""
|
||||||
|
Rank rookies using REAL career stats.
|
||||||
|
"""
|
||||||
|
|
||||||
|
db = load_career_db()
|
||||||
|
if db is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
rookies = get_rookies(db, season)
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for pid, p in rookies.items():
|
||||||
|
|
||||||
|
# Pull stats from career DB (correct source)
|
||||||
|
kills = p.get("kills", 0)
|
||||||
|
deaths = p.get("deaths", 0)
|
||||||
|
score = p.get("score", 0)
|
||||||
|
|
||||||
|
# KD
|
||||||
|
if deaths > 0:
|
||||||
|
kd = kills / deaths
|
||||||
|
else:
|
||||||
|
kd = kills
|
||||||
|
|
||||||
|
# Raw score = KD (for now)
|
||||||
|
raw = kd
|
||||||
|
|
||||||
|
# Percentile placeholder (we can compute real percentiles later)
|
||||||
|
pct = raw
|
||||||
|
|
||||||
|
# Team
|
||||||
|
team = "Unknown"
|
||||||
|
if p.get("team_history"):
|
||||||
|
team = p["team_history"][-1]
|
||||||
|
|
||||||
|
rows.append({
|
||||||
|
"id": pid,
|
||||||
|
"name": p.get("name", "Unknown"),
|
||||||
|
"team": team,
|
||||||
|
"pct": pct,
|
||||||
|
"tier": "C", # placeholder until rookie tiers are added
|
||||||
|
"raw": raw,
|
||||||
|
"kd": kd,
|
||||||
|
"kills": kills,
|
||||||
|
"deaths": deaths,
|
||||||
|
"score": score,
|
||||||
|
"summary": "",
|
||||||
})
|
})
|
||||||
|
|
||||||
rookies.sort(key=lambda r: r["tag_pct"], reverse=True)
|
# Sort by raw score
|
||||||
return rookies[:limit]
|
rows.sort(key=lambda x: x["raw"], reverse=True)
|
||||||
|
|
||||||
|
# Assign ranks
|
||||||
|
for i, r in enumerate(rows, start=1):
|
||||||
|
r["rank"] = i
|
||||||
|
|
||||||
|
return rows[:limit]
|
||||||
|
|
@ -39,9 +39,15 @@ def summarize_team_tags(team_players):
|
||||||
pcts.append(safe_pct(tag_data.get("pct")))
|
pcts.append(safe_pct(tag_data.get("pct")))
|
||||||
tiers.append(safe_tier(tag_data.get("tier")))
|
tiers.append(safe_tier(tag_data.get("tier")))
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# FIX: safe top-tier calculation
|
||||||
|
# -----------------------------
|
||||||
|
valid_tiers = [t for t in tiers if t in "SABCD"]
|
||||||
|
top_tier = max(valid_tiers, key=lambda t: "SABCD".index(t)) if valid_tiers else "-"
|
||||||
|
|
||||||
summary[tag] = {
|
summary[tag] = {
|
||||||
"avg_pct": safe_avg(pcts),
|
"avg_pct": safe_avg(pcts),
|
||||||
"top_tier": max(tiers, key=lambda t: "SABCD".index(t)),
|
"top_tier": top_tier,
|
||||||
"count_S": tiers.count("S"),
|
"count_S": tiers.count("S"),
|
||||||
"count_A": tiers.count("A"),
|
"count_A": tiers.count("A"),
|
||||||
"count_B": tiers.count("B"),
|
"count_B": tiers.count("B"),
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ from rankings import (
|
||||||
from analysis.team_identity_v2 import generate_team_identity_block
|
from analysis.team_identity_v2 import generate_team_identity_block
|
||||||
from analysis.storylines import generate_storyline, matchup_storyline
|
from analysis.storylines import generate_storyline, matchup_storyline
|
||||||
from analysis.predictions_v2 import generate_prediction
|
from analysis.predictions_v2 import generate_prediction
|
||||||
|
from analysis.rookie_leaderboard import rank_rookies_by_tag
|
||||||
|
|
||||||
|
|
||||||
# OBS export
|
# OBS export
|
||||||
|
|
@ -50,6 +51,8 @@ from match_engine import (
|
||||||
rank_match_players_clutch,
|
rank_match_players_clutch,
|
||||||
compute_slayer_prediction,
|
compute_slayer_prediction,
|
||||||
rank_match_players_consistency_career,
|
rank_match_players_consistency_career,
|
||||||
|
load_career_db,
|
||||||
|
attach_career_tags_to_match_player,
|
||||||
)
|
)
|
||||||
|
|
||||||
from data.tag_framework import percentile_rank, tier_from_percentile
|
from data.tag_framework import percentile_rank, tier_from_percentile
|
||||||
|
|
@ -235,12 +238,15 @@ class DashLeagueGUI:
|
||||||
self.rankings_button = ttk.Button(top_frame, text="Rankings", command=self.on_rankings)
|
self.rankings_button = ttk.Button(top_frame, text="Rankings", command=self.on_rankings)
|
||||||
self.rankings_button.grid(row=0, column=5, padx=(0, 10))
|
self.rankings_button.grid(row=0, column=5, padx=(0, 10))
|
||||||
|
|
||||||
|
self.rookie_button = ttk.Button(top_frame, text="Rookie Leaderboard", command=self.on_rookie_leaderboard)
|
||||||
|
self.rookie_button.grid(row=0, column=6, padx=(0, 10))
|
||||||
|
|
||||||
self.output_folder_button = ttk.Button(
|
self.output_folder_button = ttk.Button(
|
||||||
top_frame,
|
top_frame,
|
||||||
text="Select Output Folder",
|
text="Select Output Folder",
|
||||||
command=self.choose_output_folder
|
command=self.choose_output_folder
|
||||||
)
|
)
|
||||||
self.output_folder_button.grid(row=0, column=6, padx=(0, 10))
|
self.output_folder_button.grid(row=0, column=7, padx=(0, 10))
|
||||||
|
|
||||||
self.map_var = tk.StringVar()
|
self.map_var = tk.StringVar()
|
||||||
self.map_var.set("Payload") # default
|
self.map_var.set("Payload") # default
|
||||||
|
|
@ -300,11 +306,11 @@ class DashLeagueGUI:
|
||||||
url = f"{API_BASE}/stats?season={self.current_season}"
|
url = f"{API_BASE}/stats?season={self.current_season}"
|
||||||
data = fetch_json(url)
|
data = fetch_json(url)
|
||||||
stats = data.get("data", [])
|
stats = data.get("data", [])
|
||||||
|
|
||||||
print("DEBUG: Checking raw API teams...")
|
print("DEBUG: Checking raw API teams...")
|
||||||
for p in stats:
|
for p in stats:
|
||||||
print("TEAM FIELD:", repr(p.get("team")))
|
print("TEAM FIELD:", repr(p.get("team")))
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Load PIR + Career DB
|
# Load PIR + Career DB
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
|
|
@ -315,60 +321,153 @@ class DashLeagueGUI:
|
||||||
with open(career_path, "r", encoding="utf-8") as f:
|
with open(career_path, "r", encoding="utf-8") as f:
|
||||||
career_db = json.load(f)
|
career_db = json.load(f)
|
||||||
|
|
||||||
|
players_db = career_db.setdefault("players", {})
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Rebuild stats_by_id keyed by canonical ID
|
# BUILD stats_by_id (canonical → enriched player object)
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
new_stats_by_id = {}
|
new_stats_by_id = {}
|
||||||
|
|
||||||
for p in stats:
|
for p in stats:
|
||||||
raw_id = p["id"]
|
raw_id = p.get("id")
|
||||||
canonical = pir.resolve(raw_id)
|
if not raw_id:
|
||||||
|
continue
|
||||||
|
|
||||||
# Start with API stats
|
# Resolve canonical + stamp seasons
|
||||||
merged = dict(p)
|
canonical = pir.resolve(raw_id, season=self.current_season)
|
||||||
|
if not canonical:
|
||||||
|
continue
|
||||||
|
|
||||||
# 🔥 Normalize team field BEFORE anything else
|
# Ensure first_season is set if missing
|
||||||
team = merged.get("team")
|
entry = pir.data["canonical"].get(canonical)
|
||||||
if team is None:
|
#if entry is not None and entry.get("first_season") is None:
|
||||||
merged["team"] = ""
|
# entry["first_season"] = self.current_season
|
||||||
|
|
||||||
|
# Normalize DOM fields from API (for DomObj)
|
||||||
|
p["DOM_Captures"] = p.get("DOM_captures", p.get("DOM_Captures", 0))
|
||||||
|
p["DOM_Counters"] = p.get("DOM_counters", p.get("DOM_Counters", 0))
|
||||||
|
|
||||||
|
merged = {
|
||||||
|
"canonical": canonical,
|
||||||
|
"name": p.get("name"),
|
||||||
|
"team": (p.get("team") or "").strip(),
|
||||||
|
"id": raw_id,
|
||||||
|
"kills": p.get("kills", 0),
|
||||||
|
"deaths": p.get("deaths", 0),
|
||||||
|
"damage": p.get("damage", 0),
|
||||||
|
"shots": p.get("shots", 0),
|
||||||
|
"shots_hit": p.get("shots_hit", 0),
|
||||||
|
"match_stats": p,
|
||||||
|
"seasons_played": p.get("seasons_played") or p.get("seasons") or "",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ensure player exists in career DB (so rookies are present)
|
||||||
|
if canonical not in players_db:
|
||||||
|
players_db[canonical] = {
|
||||||
|
"name": merged["name"],
|
||||||
|
"team_history": [merged["team"]] if merged["team"] else [],
|
||||||
|
"seasons_played": merged.get("seasons_played"),
|
||||||
|
"slayer": {},
|
||||||
|
"sharpshooter": {},
|
||||||
|
"objective_payload": {},
|
||||||
|
"objective_domination": {},
|
||||||
|
"consistency": {},
|
||||||
|
"clutch": {},
|
||||||
|
"anchor": {},
|
||||||
|
"breaker": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
pdata = players_db.get(canonical, {})
|
||||||
|
|
||||||
|
|
||||||
|
# Store basic stats into career DB so rookies have real data
|
||||||
|
pdata["kills"] = merged.get("kills", 0)
|
||||||
|
pdata["deaths"] = merged.get("deaths", 0)
|
||||||
|
pdata["damage"] = merged.get("damage", 0)
|
||||||
|
pdata["shots"] = merged.get("shots", 0)
|
||||||
|
pdata["shots_hit"] = merged.get("shots_hit", 0)
|
||||||
|
|
||||||
|
# Score is inside match_stats
|
||||||
|
ms = merged.get("match_stats", {})
|
||||||
|
pdata["score"] = ms.get("score", 0)
|
||||||
|
|
||||||
|
# KD
|
||||||
|
if pdata["deaths"] > 0:
|
||||||
|
pdata["kd"] = pdata["kills"] / pdata["deaths"]
|
||||||
else:
|
else:
|
||||||
merged["team"] = str(team).strip()
|
pdata["kd"] = pdata["kills"]
|
||||||
|
|
||||||
# Attach canonical ID
|
# Keep seasons_played updated
|
||||||
merged["canonical_id"] = canonical
|
#if merged.get("seasons_played"):
|
||||||
|
# pdata["seasons_played"] = merged["seasons_played"]
|
||||||
|
|
||||||
|
pdata["seasons_played"] = merged.get("seasons_played")
|
||||||
|
|
||||||
# Attach career tags
|
# Attach career tags
|
||||||
pdata = career_db["players"].get(canonical)
|
for tag in (
|
||||||
if pdata:
|
"slayer",
|
||||||
merged["slayer"] = pdata.get("slayer", {})
|
"sharpshooter",
|
||||||
merged["sharpshooter"] = pdata.get("sharpshooter", {})
|
"objective_payload",
|
||||||
merged["objective_payload"] = pdata.get("objective_payload", {})
|
"objective_domination",
|
||||||
merged["objective_domination"] = pdata.get("objective_domination", {})
|
"consistency",
|
||||||
merged["consistency"] = pdata.get("consistency", {})
|
"clutch",
|
||||||
merged["clutch"] = pdata.get("clutch", {})
|
"anchor",
|
||||||
merged["anchor"] = pdata.get("anchor", {})
|
"breaker",
|
||||||
merged["breaker"] = pdata.get("breaker", {})
|
):
|
||||||
|
merged[tag] = pdata.get(tag, {})
|
||||||
|
|
||||||
new_stats_by_id[canonical] = merged
|
new_stats_by_id[canonical] = merged
|
||||||
|
|
||||||
|
# Write merged stats into career DB
|
||||||
|
players_db[canonical]["kills"] = merged["kills"]
|
||||||
|
players_db[canonical]["deaths"] = merged["deaths"]
|
||||||
|
players_db[canonical]["damage"] = merged["damage"]
|
||||||
|
players_db[canonical]["shots"] = merged["shots"]
|
||||||
|
players_db[canonical]["shots_hit"] = merged["shots_hit"]
|
||||||
|
players_db[canonical]["match_stats"] = merged["match_stats"]
|
||||||
|
players_db[canonical]["seasons_played"] = merged["seasons_played"]
|
||||||
|
|
||||||
# 🔥 Now safe to assign
|
# Compute KD
|
||||||
|
d = merged["deaths"]
|
||||||
|
players_db[canonical]["kd"] = merged["kills"] / d if d > 0 else merged["kills"]
|
||||||
|
|
||||||
|
# Score (inside match_stats)
|
||||||
|
players_db[canonical]["score"] = merged["match_stats"].get("score", 0)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Save updated career DB (rookies now included)
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
with open(career_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(career_db, f, indent=2)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Save PIR (first_season now persisted)
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
pir.save()
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Replace stats_by_id with enriched version
|
||||||
|
# ---------------------------------------------------------
|
||||||
self.stats_by_id = new_stats_by_id
|
self.stats_by_id = new_stats_by_id
|
||||||
|
|
||||||
# 🔥 Now safe to inspect teams
|
# ---------------------------------------------------------
|
||||||
|
# Debug: show teams detected
|
||||||
|
# ---------------------------------------------------------
|
||||||
teams_in_api = sorted({p["team"] for p in new_stats_by_id.values()})
|
teams_in_api = sorted({p["team"] for p in new_stats_by_id.values()})
|
||||||
print("TEAMS IN API:", teams_in_api)
|
print("TEAMS IN API:", teams_in_api)
|
||||||
|
|
||||||
self.set_status("Stats synced.")
|
self.set_status("Stats synced.")
|
||||||
messagebox.showinfo("Success", f"Synced {len(stats)} player stats.")
|
messagebox.showinfo("Success", f"Synced {len(stats)} player stats.")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
messagebox.showerror("Error", f"Failed to sync stats:\n{e}")
|
messagebox.showerror("Error", f"Failed to sync stats:\n{e}")
|
||||||
self.set_status("Sync failed.")
|
self.set_status("Sync failed.")
|
||||||
|
|
||||||
# Re-trigger team selection so listboxes repopulate
|
# Re-trigger team selection so listboxes repopulate
|
||||||
if self.team_a_var.get():
|
if self.team_a_var.get():
|
||||||
self.on_team_select("A")
|
self.on_team_select("A")
|
||||||
if self.team_b_var.get():
|
if self.team_b_var.get():
|
||||||
self.on_team_select("B")
|
self.on_team_select("B")
|
||||||
|
|
||||||
|
|
||||||
def enforce_rolling_five(self, event, side):
|
def enforce_rolling_five(self, event, side):
|
||||||
|
|
@ -448,14 +547,42 @@ class DashLeagueGUI:
|
||||||
selected_a = selected_a[:5]
|
selected_a = selected_a[:5]
|
||||||
selected_b = selected_b[:5]
|
selected_b = selected_b[:5]
|
||||||
all_players = selected_a + selected_b
|
all_players = selected_a + selected_b
|
||||||
|
|
||||||
|
print("=== DEBUG MATCH_STATS SAMPLE ===")
|
||||||
|
print(all_players[0].get("match_stats", {}))
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# 2. Normalize stat keys BEFORE attaching tags
|
# 2. Normalize stat keys BEFORE attaching tags
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
for p in all_players:
|
for p in all_players:
|
||||||
p["CP_Captures"] = p.get("CP_captures", 0)
|
ms = p.get("match_stats", {})
|
||||||
p["DOM_Captures"] = p.get("DOM_captures", 0)
|
|
||||||
p["DOM_Counters"] = p.get("DOM_counters", 0)
|
# CP
|
||||||
|
p["CP_Captures"] = (
|
||||||
|
p.get("CP_Captures")
|
||||||
|
or p.get("CP_captures")
|
||||||
|
or ms.get("CP_Captures")
|
||||||
|
or ms.get("CP_captures")
|
||||||
|
or 0
|
||||||
|
)
|
||||||
|
|
||||||
|
# DOM
|
||||||
|
p["DOM_Captures"] = (
|
||||||
|
p.get("DOM_Captures")
|
||||||
|
or p.get("DOM_captures")
|
||||||
|
or ms.get("DOM_Captures")
|
||||||
|
or ms.get("DOM_captures")
|
||||||
|
or 0
|
||||||
|
)
|
||||||
|
|
||||||
|
p["DOM_Counters"] = (
|
||||||
|
p.get("DOM_Counters")
|
||||||
|
or p.get("DOM_counters")
|
||||||
|
or ms.get("DOM_Counters")
|
||||||
|
or ms.get("DOM_counters")
|
||||||
|
or 0
|
||||||
|
)
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# 3. Resolve canonical IDs AND normalize player ID
|
# 3. Resolve canonical IDs AND normalize player ID
|
||||||
|
|
@ -471,14 +598,14 @@ class DashLeagueGUI:
|
||||||
or p.get("uuid")
|
or p.get("uuid")
|
||||||
)
|
)
|
||||||
if raw_uuid:
|
if raw_uuid:
|
||||||
p["canonical_id"] = pir.resolve(raw_uuid)
|
p["canonical"] = pir.resolve(raw_uuid, season=self.current_season)
|
||||||
|
|
||||||
# Normalize ID (AFTER canonical resolution)
|
# Normalize ID (AFTER canonical resolution)
|
||||||
pid = (
|
pid = (
|
||||||
p.get("id")
|
p.get("id")
|
||||||
or p.get("PlayerUUID")
|
or p.get("PlayerUUID")
|
||||||
or p.get("uuid")
|
or p.get("uuid")
|
||||||
or p.get("canonical_id")
|
or p.get("canonical")
|
||||||
or p.get("name")
|
or p.get("name")
|
||||||
)
|
)
|
||||||
p["id"] = pid
|
p["id"] = pid
|
||||||
|
|
@ -502,6 +629,13 @@ class DashLeagueGUI:
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# 6. Compute match-based Consistency (raw only)
|
# 6. Compute match-based Consistency (raw only)
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
print("\n=== DEBUG: Player objects BEFORE consistency ===")
|
||||||
|
for p in all_players:
|
||||||
|
print(p.get("name"), "| id:", p.get("id"), "| uuid:", p.get("uuid"), "| PlayerUUID:", p.get("PlayerUUID"), "| canonical:", p.get("canonical_id"))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
consistency_rows = rank_match_players_consistency_career(all_players)
|
consistency_rows = rank_match_players_consistency_career(all_players)
|
||||||
consistency_map = {row["id"]: row["score_raw"] for row in consistency_rows}
|
consistency_map = {row["id"]: row["score_raw"] for row in consistency_rows}
|
||||||
|
|
||||||
|
|
@ -533,7 +667,7 @@ class DashLeagueGUI:
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
if career_db is not None:
|
if career_db is not None:
|
||||||
for p in all_players:
|
for p in all_players:
|
||||||
cid = p.get("canonical_id")
|
cid = p.get("canonical")
|
||||||
if not cid:
|
if not cid:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
@ -546,10 +680,10 @@ class DashLeagueGUI:
|
||||||
p["objective_payload"] = pdata.get("objective_payload", {})
|
p["objective_payload"] = pdata.get("objective_payload", {})
|
||||||
p["clutch"] = pdata.get("clutch", {})
|
p["clutch"] = pdata.get("clutch", {})
|
||||||
p["anchor"] = pdata.get("anchor", {})
|
p["anchor"] = pdata.get("anchor", {})
|
||||||
p["breaker_career"] = pdata.get("breaker", {})
|
p["breaker"] = pdata.get("breaker", {})
|
||||||
|
|
||||||
# Do NOT overwrite match-based DomObj
|
# Do NOT overwrite match-based DomObj
|
||||||
if "objective_domination" not in p:
|
if "objective_domination" not in p or not p["objective_domination"]:
|
||||||
p["objective_domination"] = pdata.get("objective_domination", {})
|
p["objective_domination"] = pdata.get("objective_domination", {})
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
|
|
@ -561,17 +695,20 @@ class DashLeagueGUI:
|
||||||
counters = dom.get("counters", 0)
|
counters = dom.get("counters", 0)
|
||||||
dom_score = caps * 0.4 + counters * 0.6
|
dom_score = caps * 0.4 + counters * 0.6
|
||||||
|
|
||||||
p["objective_domination_components"] = {
|
print("DOM DEBUG:", p["name"], "caps=", caps, "counters=", counters)
|
||||||
|
|
||||||
|
p["objective_domination"] = {
|
||||||
"raw": dom_score,
|
"raw": dom_score,
|
||||||
|
"pct": dom_score, # <- match panel can now read this
|
||||||
|
"tier": "-", # <- neutral tier for single-match view
|
||||||
"captures": caps,
|
"captures": caps,
|
||||||
"counters": counters,
|
"counters": counters,
|
||||||
|
"summary": "", # optional, keeps shape consistent
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# 11. Ensure stats_by_id contains all enriched tags
|
||||||
# 11. Merge enriched tag data into raw match players
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
for p in all_players:
|
for p in all_players:
|
||||||
cid = p.get("canonical_id")
|
cid = p.get("canonical")
|
||||||
if not cid:
|
if not cid:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
@ -579,17 +716,10 @@ class DashLeagueGUI:
|
||||||
if not enriched:
|
if not enriched:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for key in (
|
# Copy match fields into stats_by_id
|
||||||
"slayer",
|
enriched["team"] = p.get("team")
|
||||||
"sharpshooter",
|
enriched["name"] = p.get("name")
|
||||||
"objective_payload",
|
enriched["id"] = p.get("id")
|
||||||
"objective_domination",
|
|
||||||
"clutch",
|
|
||||||
"anchor",
|
|
||||||
"breaker",
|
|
||||||
):
|
|
||||||
if key in enriched:
|
|
||||||
p[key] = enriched[key]
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# 12. Update current_match_players
|
# 12. Update current_match_players
|
||||||
|
|
@ -825,13 +955,24 @@ Consistency Summary:
|
||||||
return
|
return
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Extract players for each team (correct filtering)
|
# 1. Attach career tags to ALL match players (CRITICAL)
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
from match_engine import load_career_db, attach_career_tags_to_match_player
|
||||||
|
career_db = load_career_db()
|
||||||
|
|
||||||
|
# self.stats_by_id keys ARE canonical IDs
|
||||||
|
for cid, p in self.stats_by_id.items():
|
||||||
|
if cid in career_db["players"]:
|
||||||
|
attach_career_tags_to_match_player(p, career_db["players"][cid])
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# 2. Extract players for each team (using enriched objects)
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
teamA_players = []
|
teamA_players = []
|
||||||
teamB_players = []
|
teamB_players = []
|
||||||
|
|
||||||
for p in self.current_match_players:
|
for p in self.current_match_players:
|
||||||
cid = p.get("canonical_id")
|
cid = p.get("canonical") # <-- correct key
|
||||||
if not cid:
|
if not cid:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
@ -843,20 +984,16 @@ Consistency Summary:
|
||||||
teamA_players.append(enriched)
|
teamA_players.append(enriched)
|
||||||
elif enriched.get("team") == team_b:
|
elif enriched.get("team") == team_b:
|
||||||
teamB_players.append(enriched)
|
teamB_players.append(enriched)
|
||||||
|
|
||||||
#print("TEAM A SAMPLE:", teamA_players[0])
|
|
||||||
#print("TEAM B SAMPLE:", teamB_players[0])
|
|
||||||
|
|
||||||
if not teamA_players or not teamB_players:
|
if not teamA_players or not teamB_players:
|
||||||
messagebox.showerror("Error", "No players selected for one or both teams.")
|
messagebox.showerror("Error", "No players selected for one or both teams.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# -----------------------------------------
|
# ---------------------------------------------------------
|
||||||
# 1. Generate storyline + identity block
|
# 3. Generate storyline + identity blocks
|
||||||
# -----------------------------------------
|
# ---------------------------------------------------------
|
||||||
storyline_text = matchup_storyline(teamA_players, teamB_players)
|
storyline_text = matchup_storyline(teamA_players, teamB_players)
|
||||||
|
|
||||||
# Team identity block (per team, using modern dict + formatter)
|
|
||||||
identity_data_a = generate_team_identity_block(teamA_players, team_a)
|
identity_data_a = generate_team_identity_block(teamA_players, team_a)
|
||||||
identity_text_a = format_team_identity(identity_data_a)
|
identity_text_a = format_team_identity(identity_data_a)
|
||||||
|
|
||||||
|
|
@ -871,9 +1008,9 @@ Consistency Summary:
|
||||||
+ identity_text_b
|
+ identity_text_b
|
||||||
)
|
)
|
||||||
|
|
||||||
# -----------------------------------------
|
# ---------------------------------------------------------
|
||||||
# 2. Popup window
|
# 4. Popup window
|
||||||
# -----------------------------------------
|
# ---------------------------------------------------------
|
||||||
win = tk.Toplevel(self.root)
|
win = tk.Toplevel(self.root)
|
||||||
win.title("Match Storyline")
|
win.title("Match Storyline")
|
||||||
win.geometry("600x700")
|
win.geometry("600x700")
|
||||||
|
|
@ -882,9 +1019,9 @@ Consistency Summary:
|
||||||
output.pack(expand=True, fill="both", padx=10, pady=10)
|
output.pack(expand=True, fill="both", padx=10, pady=10)
|
||||||
output.insert(tk.END, full_story)
|
output.insert(tk.END, full_story)
|
||||||
|
|
||||||
# -----------------------------------------
|
# ---------------------------------------------------------
|
||||||
# 3. Export to OBS
|
# 5. Export to OBS
|
||||||
# -----------------------------------------
|
# ---------------------------------------------------------
|
||||||
def export():
|
def export():
|
||||||
content = output.get("1.0", tk.END).strip()
|
content = output.get("1.0", tk.END).strip()
|
||||||
if not content:
|
if not content:
|
||||||
|
|
@ -925,7 +1062,6 @@ Consistency Summary:
|
||||||
"Clutch",
|
"Clutch",
|
||||||
"Anchor",
|
"Anchor",
|
||||||
"Breaker",
|
"Breaker",
|
||||||
"Rookie",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
ttk.Label(win, text="Select Tag:").pack(anchor="w", padx=10, pady=5)
|
ttk.Label(win, text="Select Tag:").pack(anchor="w", padx=10, pady=5)
|
||||||
|
|
@ -1165,7 +1301,39 @@ Consistency Summary:
|
||||||
messagebox.showinfo("Exported", f"Rankings exported to:\n{path}")
|
messagebox.showinfo("Exported", f"Rankings exported to:\n{path}")
|
||||||
|
|
||||||
ttk.Button(win, text="Show Rankings", command=show).pack(pady=5)
|
ttk.Button(win, text="Show Rankings", command=show).pack(pady=5)
|
||||||
|
|
||||||
|
|
||||||
|
def on_rookie_leaderboard(self):
|
||||||
|
from analysis.rookie_leaderboard import rank_rookies_by_tag
|
||||||
|
|
||||||
|
season = self.current_season
|
||||||
|
tag = "slayer" # default tag for rookie leaderboard
|
||||||
|
|
||||||
|
rookies = rank_rookies_by_tag(season, tag)
|
||||||
|
|
||||||
|
# Build text output
|
||||||
|
lines = []
|
||||||
|
lines.append(f"ROOKIE LEADERBOARD — {tag.upper()} — Season {season}")
|
||||||
|
lines.append("")
|
||||||
|
lines.append(f"{'Rank':<5} {'Name':<18} {'Team':<8} {'Tier':<6} {'Pct':<6} {'Raw':<6}")
|
||||||
|
lines.append("-" * 60)
|
||||||
|
|
||||||
|
for r in rookies:
|
||||||
|
pct = f"{r['pct']:.0f}%" if r["pct"] is not None else "-"
|
||||||
|
raw = f"{r['raw']:.2f}"
|
||||||
|
lines.append(f"{r['rank']:<5} {r['name']:<18} {r['team']:<8} {r['tier']:<6} {pct:<6} {raw:<6}")
|
||||||
|
|
||||||
|
text = "\n".join(lines)
|
||||||
|
|
||||||
|
# Popup window
|
||||||
|
win = tk.Toplevel(self.root)
|
||||||
|
win.title("Rookie Leaderboard")
|
||||||
|
win.geometry("600x500")
|
||||||
|
|
||||||
|
output = tk.Text(win, wrap="word")
|
||||||
|
output.pack(expand=True, fill="both", padx=10, pady=10)
|
||||||
|
output.insert(tk.END, text)
|
||||||
|
|
||||||
|
|
||||||
def export_all_obs_files(self):
|
def export_all_obs_files(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -52,15 +52,18 @@ def build_career_database(output_path):
|
||||||
raw_uuid = str(row["PlayerUUID"])
|
raw_uuid = str(row["PlayerUUID"])
|
||||||
name = row["PlayerGameName"] or "Unknown"
|
name = row["PlayerGameName"] or "Unknown"
|
||||||
|
|
||||||
# Resolve canonical identity
|
# Get this player's full match history
|
||||||
canonical_id = pir.resolve(raw_uuid)
|
|
||||||
pir.add_name(canonical_id, name)
|
|
||||||
|
|
||||||
# Use RAW UUID for DB queries
|
|
||||||
matches = db_access.get_player_match_history(raw_uuid)
|
matches = db_access.get_player_match_history(raw_uuid)
|
||||||
if not matches:
|
if not matches:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Infer "season" from earliest CycleID (or whatever you treat as season)
|
||||||
|
first_cycle = min(m.get("CycleID", 0) for m in matches) or 0
|
||||||
|
|
||||||
|
# Resolve canonical identity AND stamp first_season
|
||||||
|
canonical_id = pir.resolve(raw_uuid, season=first_cycle)
|
||||||
|
pir.add_name(canonical_id, name)
|
||||||
|
|
||||||
# Use CANONICAL ID as the key in the career DB
|
# Use CANONICAL ID as the key in the career DB
|
||||||
pid = canonical_id
|
pid = canonical_id
|
||||||
|
|
||||||
|
|
@ -215,131 +218,50 @@ def build_career_database(output_path):
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
for pid, pdata in final_db["players"].items():
|
for pid, pdata in final_db["players"].items():
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
# Slayer
|
# Slayer
|
||||||
|
# -----------------------------------------------------
|
||||||
pdata["slayer"] = compute_slayer_for_career_player(
|
pdata["slayer"] = compute_slayer_for_career_player(
|
||||||
pdata,
|
pdata,
|
||||||
league_averages,
|
league_averages,
|
||||||
distributions["slayer"]
|
distributions["slayer"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
# Sharpshooter
|
# Sharpshooter
|
||||||
|
# -----------------------------------------------------
|
||||||
pdata["sharpshooter"] = compute_sharpshooter_for_career_player(
|
pdata["sharpshooter"] = compute_sharpshooter_for_career_player(
|
||||||
pdata["career"],
|
pdata["career"],
|
||||||
league_averages,
|
league_averages,
|
||||||
distributions["sharpshooter"]
|
distributions["sharpshooter"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
# Payload Objective Specialist
|
# Payload Objective Specialist
|
||||||
|
# -----------------------------------------------------
|
||||||
pdata["objective_payload"] = compute_payload_tag(
|
pdata["objective_payload"] = compute_payload_tag(
|
||||||
pdata,
|
pdata,
|
||||||
league_averages,
|
league_averages,
|
||||||
distributions["payload"]
|
distributions["payload"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
# Domination Objective Specialist
|
# Domination Objective Specialist
|
||||||
|
# -----------------------------------------------------
|
||||||
pdata["objective_domination"] = compute_dom_objective_for_career_player(
|
pdata["objective_domination"] = compute_dom_objective_for_career_player(
|
||||||
pdata,
|
pdata,
|
||||||
league_averages,
|
league_averages,
|
||||||
distributions["domination"]
|
distributions["domination"]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Consistency (legacy adapter)
|
|
||||||
consistency_result = build_consistency_tag(
|
|
||||||
floor_current=pdata.get("floor_current"),
|
|
||||||
floor_career=pdata.get("floor_career"),
|
|
||||||
floor_league=league_averages.get("consistency_floor", 0.0),
|
|
||||||
stability_current=pdata.get("stability_current"),
|
|
||||||
stability_career=pdata.get("stability_career"),
|
|
||||||
stability_league=league_averages.get("consistency_stability", 0.0),
|
|
||||||
average_current=pdata.get("average_current"),
|
|
||||||
average_career=pdata.get("average_career"),
|
|
||||||
average_league=league_averages.get("consistency_average", 0.0),
|
|
||||||
matches_played_current=pdata["career"].get("maps", 0),
|
|
||||||
percentile=None
|
|
||||||
)
|
|
||||||
pdata["consistency"] = {
|
|
||||||
"raw": consistency_result.normalized_score,
|
|
||||||
"summary": consistency_result.summary_short
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Consistency Percentile + Tier (NEW)
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
raw_cons = pdata["consistency"]["raw"]
|
|
||||||
|
|
||||||
# Build distribution if missing
|
|
||||||
if "consistency" not in distributions:
|
|
||||||
distributions["consistency"] = [
|
|
||||||
p["consistency"]["raw"]
|
|
||||||
for p in all_players
|
|
||||||
if "consistency" in p and isinstance(p["consistency"], dict)
|
|
||||||
]
|
|
||||||
|
|
||||||
# Percentile lookup
|
|
||||||
pct = percentile_rank(raw_cons, distributions["consistency"])
|
|
||||||
tier = tier_from_percentile(pct)
|
|
||||||
|
|
||||||
pdata["consistency"]["pct"] = pct
|
|
||||||
pdata["consistency"]["tier"] = tier
|
|
||||||
pdata["consistency"]["summary"] = (
|
|
||||||
f"{tier} Tier Consistency ({pct:.1f} percentile)"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Anchor
|
|
||||||
consistency_norm = pdata["consistency"]["raw"] # normalized 0–1 score
|
|
||||||
pdata["anchor"] = compute_anchor_for_career_player(
|
|
||||||
pdata,
|
|
||||||
league_averages,
|
|
||||||
distributions["anchor"],
|
|
||||||
consistency_norm
|
|
||||||
)
|
|
||||||
|
|
||||||
# Clutch
|
|
||||||
pdata["clutch"] = compute_clutch(
|
|
||||||
pdata,
|
|
||||||
league_averages,
|
|
||||||
clutch_distribution
|
|
||||||
)
|
|
||||||
|
|
||||||
# Breaker
|
|
||||||
career = pdata["career"]
|
|
||||||
damage_per_map = career["damage"] / max(1, career["maps"])
|
|
||||||
kills_per_map = career["kills"] / max(1, career["maps"])
|
|
||||||
|
|
||||||
raw_breaker = compute_breaker_raw(damage_per_map, kills_per_map)
|
|
||||||
|
|
||||||
# Percentile + tier
|
|
||||||
pct = percentile_rank(raw_breaker, breaker_distribution)
|
|
||||||
tier = tier_from_percentile(pct)
|
|
||||||
|
|
||||||
pdata["breaker"] = {
|
|
||||||
"raw": raw_breaker,
|
|
||||||
"pct": pct,
|
|
||||||
"tier": tier,
|
|
||||||
"summary": f"Offensive disruptor ({tier} Tier, {pct:.1f} percentile)"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Consistency (Hybrid Career Tag - New System)
|
# Consistency (Hybrid Career Tag - New System)
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
matches = pdata["matches"]
|
matches = pdata["matches"]
|
||||||
maps_played = pdata["career"].get("maps", 0)
|
maps_played = pdata["career"].get("maps", 0)
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Extract per-match performance components
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# NOTE:
|
|
||||||
# If you later add per-match slayer/objective/accuracy scores,
|
|
||||||
# this block will automatically support them.
|
|
||||||
# For now, we compute a simple per-match performance score
|
|
||||||
# using the same formula as the legacy system.
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
|
|
||||||
per_match_scores = []
|
per_match_scores = []
|
||||||
for m in matches:
|
for m in matches:
|
||||||
# Slayer-like component
|
|
||||||
dmg = m.get("Damage", 0)
|
dmg = m.get("Damage", 0)
|
||||||
kills = m.get("Kills", 0)
|
kills = m.get("Kills", 0)
|
||||||
deaths = m.get("Deaths", 0)
|
deaths = m.get("Deaths", 0)
|
||||||
|
|
@ -351,7 +273,6 @@ def build_career_database(output_path):
|
||||||
(kd / 5) * 0.20
|
(kd / 5) * 0.20
|
||||||
)
|
)
|
||||||
|
|
||||||
# Objective-like component
|
|
||||||
push = m.get("PAY_PushTime", 0)
|
push = m.get("PAY_PushTime", 0)
|
||||||
caps = m.get("DOM_Captures", 0)
|
caps = m.get("DOM_Captures", 0)
|
||||||
counters = m.get("DOM_Counters", 0)
|
counters = m.get("DOM_Counters", 0)
|
||||||
|
|
@ -362,25 +283,18 @@ def build_career_database(output_path):
|
||||||
(counters / 20) * 0.30
|
(counters / 20) * 0.30
|
||||||
)
|
)
|
||||||
|
|
||||||
# Accuracy-like component
|
|
||||||
shots = m.get("Shots", 0)
|
shots = m.get("Shots", 0)
|
||||||
shots_hit = m.get("ShotsHit", 0)
|
shots_hit = m.get("ShotsHit", 0)
|
||||||
accuracy = (shots_hit / shots) if shots > 0 else 0.0
|
accuracy = (shots_hit / shots) if shots > 0 else 0.0
|
||||||
|
|
||||||
accuracy_component = accuracy * 0.20
|
accuracy_component = accuracy * 0.20
|
||||||
|
|
||||||
# Final per-match performance score
|
|
||||||
perf = slayer_component + objective_component + accuracy_component
|
perf = slayer_component + objective_component + accuracy_component
|
||||||
per_match_scores.append(perf)
|
per_match_scores.append(perf)
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Compute floor, stability, average
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
if per_match_scores:
|
if per_match_scores:
|
||||||
floor_value = min(per_match_scores)
|
floor_value = min(per_match_scores)
|
||||||
avg_value = sum(per_match_scores) / len(per_match_scores)
|
avg_value = sum(per_match_scores) / len(per_match_scores)
|
||||||
|
|
||||||
# Variance adjusted for match count
|
|
||||||
mean = avg_value
|
mean = avg_value
|
||||||
variance = sum((x - mean) ** 2 for x in per_match_scores) / len(per_match_scores)
|
variance = sum((x - mean) ** 2 for x in per_match_scores) / len(per_match_scores)
|
||||||
adjusted_variance = variance * (1 + 1 / max(1, len(per_match_scores)))
|
adjusted_variance = variance * (1 + 1 / max(1, len(per_match_scores)))
|
||||||
|
|
@ -390,12 +304,9 @@ def build_career_database(output_path):
|
||||||
avg_value = 0.0
|
avg_value = 0.0
|
||||||
stability_value = 0.0
|
stability_value = 0.0
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Build hybrid consistency tag
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
consistency_result = build_consistency_tag(
|
consistency_result = build_consistency_tag(
|
||||||
floor_current=floor_value,
|
floor_current=floor_value,
|
||||||
floor_career=floor_value, # career = all matches
|
floor_career=floor_value,
|
||||||
floor_league=league_averages.get("consistency_floor", 0.0),
|
floor_league=league_averages.get("consistency_floor", 0.0),
|
||||||
|
|
||||||
stability_current=stability_value,
|
stability_current=stability_value,
|
||||||
|
|
@ -410,12 +321,9 @@ def build_career_database(output_path):
|
||||||
percentile=None
|
percentile=None
|
||||||
)
|
)
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Store in DB (GUI + Rankings + Spotlight compatible)
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
pdata["consistency"] = {
|
pdata["consistency"] = {
|
||||||
"raw": consistency_result.normalized_score,
|
"raw": consistency_result.normalized_score,
|
||||||
"pct": consistency_result.percentile,
|
"pct": None,
|
||||||
"tier": consistency_result.tier,
|
"tier": consistency_result.tier,
|
||||||
"summary": consistency_result.summary_short,
|
"summary": consistency_result.summary_short,
|
||||||
"components": {
|
"components": {
|
||||||
|
|
@ -425,14 +333,63 @@ def build_career_database(output_path):
|
||||||
},
|
},
|
||||||
"extras": consistency_result.extras,
|
"extras": consistency_result.extras,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Career Consistency Percentile + Tier (NEW)
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
raw_cons = pdata["consistency"]["raw"]
|
||||||
|
cons_dist = distributions.get("consistency", [])
|
||||||
|
|
||||||
# Clutch (FINAL TAG)
|
pct = percentile_rank(raw_cons, cons_dist)
|
||||||
|
tier = tier_from_percentile(pct)
|
||||||
|
|
||||||
|
pdata["consistency"]["pct"] = pct
|
||||||
|
pdata["consistency"]["tier"] = tier
|
||||||
|
pdata["consistency"]["summary"] = (
|
||||||
|
f"{tier} Tier Consistency ({pct:.1f} percentile)"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Anchor (requires consistency)
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
consistency_norm = pdata["consistency"]["raw"]
|
||||||
|
pdata["anchor"] = compute_anchor_for_career_player(
|
||||||
|
pdata,
|
||||||
|
league_averages,
|
||||||
|
distributions["anchor"],
|
||||||
|
consistency_norm
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Clutch
|
||||||
|
# ---------------------------------------------------------
|
||||||
pdata["clutch"] = compute_clutch(
|
pdata["clutch"] = compute_clutch(
|
||||||
pdata,
|
pdata,
|
||||||
league_averages,
|
league_averages,
|
||||||
clutch_distribution
|
clutch_distribution
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Breaker
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
career = pdata["career"]
|
||||||
|
damage_per_map = career["damage"] / max(1, career["maps"])
|
||||||
|
kills_per_map = career["kills"] / max(1, career["maps"])
|
||||||
|
|
||||||
|
raw_breaker = compute_breaker_raw(damage_per_map, kills_per_map)
|
||||||
|
pct = percentile_rank(raw_breaker, breaker_distribution)
|
||||||
|
tier = tier_from_percentile(pct)
|
||||||
|
|
||||||
|
pdata["breaker"] = {
|
||||||
|
"raw": raw_breaker,
|
||||||
|
"pct": pct,
|
||||||
|
"tier": tier,
|
||||||
|
"summary": f"Offensive disruptor ({tier} Tier, {pct:.1f} percentile)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Save PIR with first_season stamped for all canonical IDs
|
||||||
|
pir.save()
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# 6. Save DB
|
# 6. Save DB
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -210,6 +210,7 @@ def rank_match_players_consistency_career(match_players):
|
||||||
for i, p in enumerate(ranked, 1):
|
for i, p in enumerate(ranked, 1):
|
||||||
out.append({
|
out.append({
|
||||||
"rank": i,
|
"rank": i,
|
||||||
|
"id": p["id"],
|
||||||
"name": p["name"],
|
"name": p["name"],
|
||||||
"team": p.get("team", "Unknown"),
|
"team": p.get("team", "Unknown"),
|
||||||
"score_raw": p["raw"],
|
"score_raw": p["raw"],
|
||||||
|
|
|
||||||
55
rankings.py
55
rankings.py
|
|
@ -58,7 +58,7 @@ def top_players_by_tag(tag_name, limit=20):
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Match-Based Consistency Ranking (Still Used in UI)
|
# Match-Based Consistency Ranking (Modernized)
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
def rank_match_players_consistency(players):
|
def rank_match_players_consistency(players):
|
||||||
|
|
@ -66,13 +66,17 @@ def rank_match_players_consistency(players):
|
||||||
|
|
||||||
for p in players:
|
for p in players:
|
||||||
tag = p.get("consistency", {})
|
tag = p.get("consistency", {})
|
||||||
score = tag.get("raw", 0.0)
|
raw = tag.get("raw", 0.0)
|
||||||
|
pct = tag.get("pct", None)
|
||||||
|
tier = tag.get("tier", None)
|
||||||
|
|
||||||
ranked.append({
|
ranked.append({
|
||||||
"name": p.get("name", "Unknown"),
|
"name": p.get("name", "Unknown"),
|
||||||
"team": p.get("team", ""),
|
"team": p.get("team", ""),
|
||||||
"score_raw": score,
|
"score_raw": raw,
|
||||||
"score_display": f"{score:.2f}",
|
"score_display": f"{raw:.2f}",
|
||||||
|
"pct": pct,
|
||||||
|
"tier": tier,
|
||||||
})
|
})
|
||||||
|
|
||||||
ranked.sort(key=lambda x: x["score_raw"], reverse=True)
|
ranked.sort(key=lambda x: x["score_raw"], reverse=True)
|
||||||
|
|
@ -84,7 +88,7 @@ def rank_match_players_consistency(players):
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Match-Based Clutch Ranking (Still Used in UI)
|
# Match-Based Clutch Ranking (Modernized)
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
def rank_match_players_clutch(players):
|
def rank_match_players_clutch(players):
|
||||||
|
|
@ -92,13 +96,17 @@ def rank_match_players_clutch(players):
|
||||||
|
|
||||||
for p in players:
|
for p in players:
|
||||||
tag = p.get("clutch", {})
|
tag = p.get("clutch", {})
|
||||||
score = tag.get("raw", 0.0)
|
raw = tag.get("raw", 0.0)
|
||||||
|
pct = tag.get("pct", None)
|
||||||
|
tier = tag.get("tier", None)
|
||||||
|
|
||||||
ranked.append({
|
ranked.append({
|
||||||
"name": p.get("name", "Unknown"),
|
"name": p.get("name", "Unknown"),
|
||||||
"team": p.get("team", ""),
|
"team": p.get("team", ""),
|
||||||
"score_raw": score,
|
"score_raw": raw,
|
||||||
"score_display": f"{score:.2f}",
|
"score_display": f"{raw:.2f}",
|
||||||
|
"pct": pct,
|
||||||
|
"tier": tier,
|
||||||
})
|
})
|
||||||
|
|
||||||
ranked.sort(key=lambda x: x["score_raw"], reverse=True)
|
ranked.sort(key=lambda x: x["score_raw"], reverse=True)
|
||||||
|
|
@ -107,25 +115,28 @@ def rank_match_players_clutch(players):
|
||||||
r["rank"] = i
|
r["rank"] = i
|
||||||
|
|
||||||
return ranked
|
return ranked
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Generic Match Tag Ranking (Modern Tag System)
|
# Generic Match Tag Ranking (Modern Tag System)
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
def rank_match_players_by_tag(players, tag):
|
def rank_match_players_by_tag(players, tag):
|
||||||
rows = []
|
rows = []
|
||||||
|
|
||||||
for p in players:
|
for p in players:
|
||||||
tag_data = p.get(tag) or {} # <‑‑‑ FIX HERE
|
tag_data = p.get(tag) or {}
|
||||||
raw = tag_data.get("raw", 0)
|
raw = tag_data.get("raw", 0.0)
|
||||||
|
pct = tag_data.get("pct", None)
|
||||||
|
tier = tag_data.get("tier", None)
|
||||||
|
|
||||||
rows.append({
|
rows.append({
|
||||||
"name": p.get("name", "Unknown"),
|
"name": p.get("name", "Unknown"),
|
||||||
"team": p.get("team", ""),
|
"team": p.get("team", ""),
|
||||||
"score_raw": raw,
|
"score_raw": raw,
|
||||||
"score_display": f"{raw:.2f}",
|
"score_display": f"{raw:.2f}",
|
||||||
|
"pct": pct,
|
||||||
|
"tier": tier,
|
||||||
})
|
})
|
||||||
|
|
||||||
rows.sort(key=lambda x: x["score_raw"], reverse=True)
|
rows.sort(key=lambda x: x["score_raw"], reverse=True)
|
||||||
|
|
@ -134,13 +145,13 @@ def rank_match_players_by_tag(players, tag):
|
||||||
r["rank"] = i
|
r["rank"] = i
|
||||||
|
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Split into two columns for UI
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
def split_two_columns(ranked, team_a, team_b):
|
def split_two_columns(ranked, team_a, team_b):
|
||||||
"""
|
|
||||||
ranked: list of rows returned by rank_match_players_* functions.
|
|
||||||
Returns two lists: left (team_a) and right (team_b).
|
|
||||||
"""
|
|
||||||
left = [r for r in ranked if r.get("team") == team_a]
|
left = [r for r in ranked if r.get("team") == team_a]
|
||||||
right = [r for r in ranked if r.get("team") == team_b]
|
right = [r for r in ranked if r.get("team") == team_b]
|
||||||
return left, right
|
return left, right
|
||||||
Loading…
Reference in a new issue