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")
|
||||
|
||||
|
||||
class PlayerIdentityRegistry:
|
||||
def __init__(self):
|
||||
if os.path.exists(PIR_PATH):
|
||||
with open(PIR_PATH, "r", encoding="utf-8") as f:
|
||||
self.data = json.load(f)
|
||||
else:
|
||||
self.data = {"uuid_to_canonical": {}, "canonical": {}}
|
||||
self.data = {
|
||||
"uuid_to_canonical": {},
|
||||
"canonical": {}
|
||||
}
|
||||
|
||||
def save(self):
|
||||
with open(PIR_PATH, "w", encoding="utf-8") as f:
|
||||
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)
|
||||
|
||||
# Known UUID
|
||||
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
|
||||
canonical = f"player_{len(self.data['canonical'])+1}"
|
||||
self.data["uuid_to_canonical"][uuid] = canonical
|
||||
self.data["canonical"][canonical] = {"uuids": [uuid], "names": []}
|
||||
# New UUID → new canonical ID
|
||||
cid = f"player_{len(self.data['canonical']) + 1}"
|
||||
self.data["uuid_to_canonical"][uuid] = cid
|
||||
self.data["canonical"][cid] = {
|
||||
"uuids": [uuid],
|
||||
"names": [],
|
||||
"team_history": [],
|
||||
"first_season": season,
|
||||
"last_season": season,
|
||||
}
|
||||
self.save()
|
||||
return canonical
|
||||
return cid
|
||||
|
||||
def add_name(self, canonical, name):
|
||||
entry = self.data["canonical"].setdefault(canonical, {"uuids": [], "names": []})
|
||||
if name not in entry["names"]:
|
||||
# ---------------------------------------------------------
|
||||
# Track name changes
|
||||
# ---------------------------------------------------------
|
||||
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)
|
||||
self.save()
|
||||
|
||||
def merge(self, uuid_a, uuid_b):
|
||||
"""Merge two UUIDs into one canonical identity."""
|
||||
ca = self.resolve(uuid_a)
|
||||
cb = self.resolve(uuid_b)
|
||||
if ca == cb:
|
||||
return ca
|
||||
# ---------------------------------------------------------
|
||||
# Track team history
|
||||
# ---------------------------------------------------------
|
||||
def update_team(self, canonical_id, team):
|
||||
entry = self.data["canonical"].get(canonical_id)
|
||||
if not entry:
|
||||
return
|
||||
|
||||
# Merge cb into ca
|
||||
self.data["canonical"][ca]["uuids"].extend(self.data["canonical"][cb]["uuids"])
|
||||
self.data["canonical"][ca]["names"].extend(self.data["canonical"][cb]["names"])
|
||||
if team and (not entry["team_history"] or entry["team_history"][-1] != team):
|
||||
entry["team_history"].append(team)
|
||||
self.save()
|
||||
|
||||
# Update uuid_to_canonical
|
||||
for u in self.data["canonical"][cb]["uuids"]:
|
||||
self.data["uuid_to_canonical"][u] = ca
|
||||
# ---------------------------------------------------------
|
||||
# Backwards compatibility: old method name
|
||||
# ---------------------------------------------------------
|
||||
def add_name(self, canonical_id, name):
|
||||
"""Legacy alias for update_name()."""
|
||||
self.update_name(canonical_id, name)
|
||||
|
||||
del self.data["canonical"][cb]
|
||||
self.save()
|
||||
return ca
|
||||
def add_team(self, canonical_id, team):
|
||||
"""Legacy alias for update_team()."""
|
||||
self.update_team(canonical_id, team)
|
||||
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Rookie detection
|
||||
# ---------------------------------------------------------
|
||||
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):
|
||||
rookies = []
|
||||
import os
|
||||
import json
|
||||
|
||||
for cid, pdata in career_db["players"].items():
|
||||
maps = pdata["career"].get("maps", 0)
|
||||
if maps >= 10:
|
||||
continue # not a rookie
|
||||
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
CAREER_DB_PATH = os.path.join(PROJECT_ROOT, "career_stats.json")
|
||||
|
||||
tag_data = pdata.get(tag, {})
|
||||
rookies.append({
|
||||
"canonical_id": cid,
|
||||
"name": pdata["name"],
|
||||
"team": pdata.get("team", ""),
|
||||
"maps": maps,
|
||||
"kd": pdata["career"]["KD"],
|
||||
"kills": pdata["career"]["kills"],
|
||||
"score": pdata["career"]["damage"], # or actual score if available
|
||||
"tag_raw": tag_data.get("raw", 0.0),
|
||||
"tag_pct": tag_data.get("pct", 0.0),
|
||||
"tag_tier": tag_data.get("tier", "D"),
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Load DB
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def load_career_db():
|
||||
if not os.path.exists(CAREER_DB_PATH):
|
||||
return None
|
||||
with open(CAREER_DB_PATH, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 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)
|
||||
return rookies[:limit]
|
||||
# Sort by raw score
|
||||
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")))
|
||||
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] = {
|
||||
"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_A": tiers.count("A"),
|
||||
"count_B": tiers.count("B"),
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ from rankings import (
|
|||
from analysis.team_identity_v2 import generate_team_identity_block
|
||||
from analysis.storylines import generate_storyline, matchup_storyline
|
||||
from analysis.predictions_v2 import generate_prediction
|
||||
from analysis.rookie_leaderboard import rank_rookies_by_tag
|
||||
|
||||
|
||||
# OBS export
|
||||
|
|
@ -50,6 +51,8 @@ from match_engine import (
|
|||
rank_match_players_clutch,
|
||||
compute_slayer_prediction,
|
||||
rank_match_players_consistency_career,
|
||||
load_career_db,
|
||||
attach_career_tags_to_match_player,
|
||||
)
|
||||
|
||||
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.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(
|
||||
top_frame,
|
||||
text="Select 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.set("Payload") # default
|
||||
|
|
@ -315,51 +321,144 @@ class DashLeagueGUI:
|
|||
with open(career_path, "r", encoding="utf-8") as 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 = {}
|
||||
|
||||
for p in stats:
|
||||
raw_id = p["id"]
|
||||
canonical = pir.resolve(raw_id)
|
||||
raw_id = p.get("id")
|
||||
if not raw_id:
|
||||
continue
|
||||
|
||||
# Start with API stats
|
||||
merged = dict(p)
|
||||
# Resolve canonical + stamp seasons
|
||||
canonical = pir.resolve(raw_id, season=self.current_season)
|
||||
if not canonical:
|
||||
continue
|
||||
|
||||
# 🔥 Normalize team field BEFORE anything else
|
||||
team = merged.get("team")
|
||||
if team is None:
|
||||
merged["team"] = ""
|
||||
# Ensure first_season is set if missing
|
||||
entry = pir.data["canonical"].get(canonical)
|
||||
#if entry is not None and entry.get("first_season") is None:
|
||||
# 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:
|
||||
merged["team"] = str(team).strip()
|
||||
pdata["kd"] = pdata["kills"]
|
||||
|
||||
# Attach canonical ID
|
||||
merged["canonical_id"] = canonical
|
||||
# Keep seasons_played updated
|
||||
#if merged.get("seasons_played"):
|
||||
# pdata["seasons_played"] = merged["seasons_played"]
|
||||
|
||||
pdata["seasons_played"] = merged.get("seasons_played")
|
||||
|
||||
# Attach career tags
|
||||
pdata = career_db["players"].get(canonical)
|
||||
if pdata:
|
||||
merged["slayer"] = pdata.get("slayer", {})
|
||||
merged["sharpshooter"] = pdata.get("sharpshooter", {})
|
||||
merged["objective_payload"] = pdata.get("objective_payload", {})
|
||||
merged["objective_domination"] = pdata.get("objective_domination", {})
|
||||
merged["consistency"] = pdata.get("consistency", {})
|
||||
merged["clutch"] = pdata.get("clutch", {})
|
||||
merged["anchor"] = pdata.get("anchor", {})
|
||||
merged["breaker"] = pdata.get("breaker", {})
|
||||
for tag in (
|
||||
"slayer",
|
||||
"sharpshooter",
|
||||
"objective_payload",
|
||||
"objective_domination",
|
||||
"consistency",
|
||||
"clutch",
|
||||
"anchor",
|
||||
"breaker",
|
||||
):
|
||||
merged[tag] = pdata.get(tag, {})
|
||||
|
||||
new_stats_by_id[canonical] = merged
|
||||
|
||||
# 🔥 Now safe to assign
|
||||
# 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"]
|
||||
|
||||
# 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
|
||||
|
||||
# 🔥 Now safe to inspect teams
|
||||
# ---------------------------------------------------------
|
||||
# Debug: show teams detected
|
||||
# ---------------------------------------------------------
|
||||
teams_in_api = sorted({p["team"] for p in new_stats_by_id.values()})
|
||||
print("TEAMS IN API:", teams_in_api)
|
||||
|
||||
self.set_status("Stats synced.")
|
||||
messagebox.showinfo("Success", f"Synced {len(stats)} player stats.")
|
||||
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Failed to sync stats:\n{e}")
|
||||
self.set_status("Sync failed.")
|
||||
|
|
@ -449,13 +548,41 @@ class DashLeagueGUI:
|
|||
selected_b = selected_b[:5]
|
||||
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
|
||||
# ---------------------------------------------------------
|
||||
|
||||
for p in all_players:
|
||||
p["CP_Captures"] = p.get("CP_captures", 0)
|
||||
p["DOM_Captures"] = p.get("DOM_captures", 0)
|
||||
p["DOM_Counters"] = p.get("DOM_counters", 0)
|
||||
ms = p.get("match_stats", {})
|
||||
|
||||
# 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
|
||||
|
|
@ -471,14 +598,14 @@ class DashLeagueGUI:
|
|||
or p.get("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)
|
||||
pid = (
|
||||
p.get("id")
|
||||
or p.get("PlayerUUID")
|
||||
or p.get("uuid")
|
||||
or p.get("canonical_id")
|
||||
or p.get("canonical")
|
||||
or p.get("name")
|
||||
)
|
||||
p["id"] = pid
|
||||
|
|
@ -502,6 +629,13 @@ class DashLeagueGUI:
|
|||
# ---------------------------------------------------------
|
||||
# 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_map = {row["id"]: row["score_raw"] for row in consistency_rows}
|
||||
|
||||
|
|
@ -533,7 +667,7 @@ class DashLeagueGUI:
|
|||
# ---------------------------------------------------------
|
||||
if career_db is not None:
|
||||
for p in all_players:
|
||||
cid = p.get("canonical_id")
|
||||
cid = p.get("canonical")
|
||||
if not cid:
|
||||
continue
|
||||
|
||||
|
|
@ -546,10 +680,10 @@ class DashLeagueGUI:
|
|||
p["objective_payload"] = pdata.get("objective_payload", {})
|
||||
p["clutch"] = pdata.get("clutch", {})
|
||||
p["anchor"] = pdata.get("anchor", {})
|
||||
p["breaker_career"] = pdata.get("breaker", {})
|
||||
p["breaker"] = pdata.get("breaker", {})
|
||||
|
||||
# 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", {})
|
||||
|
||||
# ---------------------------------------------------------
|
||||
|
|
@ -561,17 +695,20 @@ class DashLeagueGUI:
|
|||
counters = dom.get("counters", 0)
|
||||
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,
|
||||
"pct": dom_score, # <- match panel can now read this
|
||||
"tier": "-", # <- neutral tier for single-match view
|
||||
"captures": caps,
|
||||
"counters": counters,
|
||||
"summary": "", # optional, keeps shape consistent
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 11. Merge enriched tag data into raw match players
|
||||
# ---------------------------------------------------------
|
||||
# 11. Ensure stats_by_id contains all enriched tags
|
||||
for p in all_players:
|
||||
cid = p.get("canonical_id")
|
||||
cid = p.get("canonical")
|
||||
if not cid:
|
||||
continue
|
||||
|
||||
|
|
@ -579,17 +716,10 @@ class DashLeagueGUI:
|
|||
if not enriched:
|
||||
continue
|
||||
|
||||
for key in (
|
||||
"slayer",
|
||||
"sharpshooter",
|
||||
"objective_payload",
|
||||
"objective_domination",
|
||||
"clutch",
|
||||
"anchor",
|
||||
"breaker",
|
||||
):
|
||||
if key in enriched:
|
||||
p[key] = enriched[key]
|
||||
# Copy match fields into stats_by_id
|
||||
enriched["team"] = p.get("team")
|
||||
enriched["name"] = p.get("name")
|
||||
enriched["id"] = p.get("id")
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 12. Update current_match_players
|
||||
|
|
@ -825,13 +955,24 @@ Consistency Summary:
|
|||
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 = []
|
||||
teamB_players = []
|
||||
|
||||
for p in self.current_match_players:
|
||||
cid = p.get("canonical_id")
|
||||
cid = p.get("canonical") # <-- correct key
|
||||
if not cid:
|
||||
continue
|
||||
|
||||
|
|
@ -844,19 +985,15 @@ Consistency Summary:
|
|||
elif enriched.get("team") == team_b:
|
||||
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:
|
||||
messagebox.showerror("Error", "No players selected for one or both teams.")
|
||||
return
|
||||
|
||||
# -----------------------------------------
|
||||
# 1. Generate storyline + identity block
|
||||
# -----------------------------------------
|
||||
# ---------------------------------------------------------
|
||||
# 3. Generate storyline + identity blocks
|
||||
# ---------------------------------------------------------
|
||||
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_text_a = format_team_identity(identity_data_a)
|
||||
|
||||
|
|
@ -871,9 +1008,9 @@ Consistency Summary:
|
|||
+ identity_text_b
|
||||
)
|
||||
|
||||
# -----------------------------------------
|
||||
# 2. Popup window
|
||||
# -----------------------------------------
|
||||
# ---------------------------------------------------------
|
||||
# 4. Popup window
|
||||
# ---------------------------------------------------------
|
||||
win = tk.Toplevel(self.root)
|
||||
win.title("Match Storyline")
|
||||
win.geometry("600x700")
|
||||
|
|
@ -882,9 +1019,9 @@ Consistency Summary:
|
|||
output.pack(expand=True, fill="both", padx=10, pady=10)
|
||||
output.insert(tk.END, full_story)
|
||||
|
||||
# -----------------------------------------
|
||||
# 3. Export to OBS
|
||||
# -----------------------------------------
|
||||
# ---------------------------------------------------------
|
||||
# 5. Export to OBS
|
||||
# ---------------------------------------------------------
|
||||
def export():
|
||||
content = output.get("1.0", tk.END).strip()
|
||||
if not content:
|
||||
|
|
@ -925,7 +1062,6 @@ Consistency Summary:
|
|||
"Clutch",
|
||||
"Anchor",
|
||||
"Breaker",
|
||||
"Rookie",
|
||||
]
|
||||
|
||||
ttk.Label(win, text="Select Tag:").pack(anchor="w", padx=10, pady=5)
|
||||
|
|
@ -1167,6 +1303,38 @@ Consistency Summary:
|
|||
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):
|
||||
"""
|
||||
Unified export function that writes all OBS text files
|
||||
|
|
|
|||
|
|
@ -52,15 +52,18 @@ def build_career_database(output_path):
|
|||
raw_uuid = str(row["PlayerUUID"])
|
||||
name = row["PlayerGameName"] or "Unknown"
|
||||
|
||||
# Resolve canonical identity
|
||||
canonical_id = pir.resolve(raw_uuid)
|
||||
pir.add_name(canonical_id, name)
|
||||
|
||||
# Use RAW UUID for DB queries
|
||||
# Get this player's full match history
|
||||
matches = db_access.get_player_match_history(raw_uuid)
|
||||
if not matches:
|
||||
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
|
||||
pid = canonical_id
|
||||
|
||||
|
|
@ -215,131 +218,50 @@ def build_career_database(output_path):
|
|||
# ---------------------------------------------------------
|
||||
for pid, pdata in final_db["players"].items():
|
||||
|
||||
# -----------------------------------------------------
|
||||
# Slayer
|
||||
# -----------------------------------------------------
|
||||
pdata["slayer"] = compute_slayer_for_career_player(
|
||||
pdata,
|
||||
league_averages,
|
||||
distributions["slayer"]
|
||||
)
|
||||
|
||||
# -----------------------------------------------------
|
||||
# Sharpshooter
|
||||
# -----------------------------------------------------
|
||||
pdata["sharpshooter"] = compute_sharpshooter_for_career_player(
|
||||
pdata["career"],
|
||||
league_averages,
|
||||
distributions["sharpshooter"]
|
||||
)
|
||||
|
||||
# -----------------------------------------------------
|
||||
# Payload Objective Specialist
|
||||
# -----------------------------------------------------
|
||||
pdata["objective_payload"] = compute_payload_tag(
|
||||
pdata,
|
||||
league_averages,
|
||||
distributions["payload"]
|
||||
)
|
||||
|
||||
# -----------------------------------------------------
|
||||
# Domination Objective Specialist
|
||||
# -----------------------------------------------------
|
||||
pdata["objective_domination"] = compute_dom_objective_for_career_player(
|
||||
pdata,
|
||||
league_averages,
|
||||
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)
|
||||
# ---------------------------------------------------------
|
||||
|
||||
matches = pdata["matches"]
|
||||
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 = []
|
||||
for m in matches:
|
||||
# Slayer-like component
|
||||
dmg = m.get("Damage", 0)
|
||||
kills = m.get("Kills", 0)
|
||||
deaths = m.get("Deaths", 0)
|
||||
|
|
@ -351,7 +273,6 @@ def build_career_database(output_path):
|
|||
(kd / 5) * 0.20
|
||||
)
|
||||
|
||||
# Objective-like component
|
||||
push = m.get("PAY_PushTime", 0)
|
||||
caps = m.get("DOM_Captures", 0)
|
||||
counters = m.get("DOM_Counters", 0)
|
||||
|
|
@ -362,25 +283,18 @@ def build_career_database(output_path):
|
|||
(counters / 20) * 0.30
|
||||
)
|
||||
|
||||
# Accuracy-like component
|
||||
shots = m.get("Shots", 0)
|
||||
shots_hit = m.get("ShotsHit", 0)
|
||||
accuracy = (shots_hit / shots) if shots > 0 else 0.0
|
||||
|
||||
accuracy_component = accuracy * 0.20
|
||||
|
||||
# Final per-match performance score
|
||||
perf = slayer_component + objective_component + accuracy_component
|
||||
per_match_scores.append(perf)
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Compute floor, stability, average
|
||||
# ---------------------------------------------------------
|
||||
if per_match_scores:
|
||||
floor_value = min(per_match_scores)
|
||||
avg_value = sum(per_match_scores) / len(per_match_scores)
|
||||
|
||||
# Variance adjusted for match count
|
||||
mean = avg_value
|
||||
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)))
|
||||
|
|
@ -390,12 +304,9 @@ def build_career_database(output_path):
|
|||
avg_value = 0.0
|
||||
stability_value = 0.0
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Build hybrid consistency tag
|
||||
# ---------------------------------------------------------
|
||||
consistency_result = build_consistency_tag(
|
||||
floor_current=floor_value,
|
||||
floor_career=floor_value, # career = all matches
|
||||
floor_career=floor_value,
|
||||
floor_league=league_averages.get("consistency_floor", 0.0),
|
||||
|
||||
stability_current=stability_value,
|
||||
|
|
@ -410,12 +321,9 @@ def build_career_database(output_path):
|
|||
percentile=None
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Store in DB (GUI + Rankings + Spotlight compatible)
|
||||
# ---------------------------------------------------------
|
||||
pdata["consistency"] = {
|
||||
"raw": consistency_result.normalized_score,
|
||||
"pct": consistency_result.percentile,
|
||||
"pct": None,
|
||||
"tier": consistency_result.tier,
|
||||
"summary": consistency_result.summary_short,
|
||||
"components": {
|
||||
|
|
@ -426,13 +334,62 @@ def build_career_database(output_path):
|
|||
"extras": consistency_result.extras,
|
||||
}
|
||||
|
||||
# Clutch (FINAL TAG)
|
||||
# ---------------------------------------------------------
|
||||
# Career Consistency Percentile + Tier (NEW)
|
||||
# ---------------------------------------------------------
|
||||
raw_cons = pdata["consistency"]["raw"]
|
||||
cons_dist = distributions.get("consistency", [])
|
||||
|
||||
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,
|
||||
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)
|
||||
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
|
||||
# ---------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -210,6 +210,7 @@ def rank_match_players_consistency_career(match_players):
|
|||
for i, p in enumerate(ranked, 1):
|
||||
out.append({
|
||||
"rank": i,
|
||||
"id": p["id"],
|
||||
"name": p["name"],
|
||||
"team": p.get("team", "Unknown"),
|
||||
"score_raw": p["raw"],
|
||||
|
|
|
|||
41
rankings.py
41
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):
|
||||
|
|
@ -66,13 +66,17 @@ def rank_match_players_consistency(players):
|
|||
|
||||
for p in players:
|
||||
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({
|
||||
"name": p.get("name", "Unknown"),
|
||||
"team": p.get("team", ""),
|
||||
"score_raw": score,
|
||||
"score_display": f"{score:.2f}",
|
||||
"score_raw": raw,
|
||||
"score_display": f"{raw:.2f}",
|
||||
"pct": pct,
|
||||
"tier": tier,
|
||||
})
|
||||
|
||||
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):
|
||||
|
|
@ -92,13 +96,17 @@ def rank_match_players_clutch(players):
|
|||
|
||||
for p in players:
|
||||
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({
|
||||
"name": p.get("name", "Unknown"),
|
||||
"team": p.get("team", ""),
|
||||
"score_raw": score,
|
||||
"score_display": f"{score:.2f}",
|
||||
"score_raw": raw,
|
||||
"score_display": f"{raw:.2f}",
|
||||
"pct": pct,
|
||||
"tier": tier,
|
||||
})
|
||||
|
||||
ranked.sort(key=lambda x: x["score_raw"], reverse=True)
|
||||
|
|
@ -109,7 +117,6 @@ def rank_match_players_clutch(players):
|
|||
return ranked
|
||||
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Generic Match Tag Ranking (Modern Tag System)
|
||||
# ---------------------------------------------------------
|
||||
|
|
@ -118,14 +125,18 @@ def rank_match_players_by_tag(players, tag):
|
|||
rows = []
|
||||
|
||||
for p in players:
|
||||
tag_data = p.get(tag) or {} # <‑‑‑ FIX HERE
|
||||
raw = tag_data.get("raw", 0)
|
||||
tag_data = p.get(tag) or {}
|
||||
raw = tag_data.get("raw", 0.0)
|
||||
pct = tag_data.get("pct", None)
|
||||
tier = tag_data.get("tier", None)
|
||||
|
||||
rows.append({
|
||||
"name": p.get("name", "Unknown"),
|
||||
"team": p.get("team", ""),
|
||||
"score_raw": raw,
|
||||
"score_display": f"{raw:.2f}",
|
||||
"pct": pct,
|
||||
"tier": tier,
|
||||
})
|
||||
|
||||
rows.sort(key=lambda x: x["score_raw"], reverse=True)
|
||||
|
|
@ -136,11 +147,11 @@ def rank_match_players_by_tag(players, tag):
|
|||
return rows
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Split into two columns for UI
|
||||
# ---------------------------------------------------------
|
||||
|
||||
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]
|
||||
right = [r for r in ranked if r.get("team") == team_b]
|
||||
return left, right
|
||||
Loading…
Reference in a new issue