Milestone: Career DB rebuilt, canonical-ID sync stable, storyline tags correct. Pending: match ranking anchor/breaker.
This commit is contained in:
parent
ef88da9c53
commit
cbebd5820a
15 changed files with 8305 additions and 228 deletions
7755
analysis/player_identity_registry.json
Normal file
7755
analysis/player_identity_registry.json
Normal file
File diff suppressed because it is too large
Load diff
54
analysis/player_identity_registry.py
Normal file
54
analysis/player_identity_registry.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import json
|
||||
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": {}}
|
||||
|
||||
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."""
|
||||
uuid = str(uuid)
|
||||
if uuid in self.data["uuid_to_canonical"]:
|
||||
return self.data["uuid_to_canonical"][uuid]
|
||||
|
||||
# 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": []}
|
||||
self.save()
|
||||
return canonical
|
||||
|
||||
def add_name(self, canonical, name):
|
||||
entry = self.data["canonical"].setdefault(canonical, {"uuids": [], "names": []})
|
||||
if 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
|
||||
|
||||
# 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"])
|
||||
|
||||
# Update uuid_to_canonical
|
||||
for u in self.data["canonical"][cb]["uuids"]:
|
||||
self.data["uuid_to_canonical"][u] = ca
|
||||
|
||||
del self.data["canonical"][cb]
|
||||
self.save()
|
||||
return ca
|
||||
|
|
@ -7,7 +7,6 @@ def build_player_summary(player):
|
|||
- Sharpshooter
|
||||
- Consistency
|
||||
- Clutch
|
||||
- Support Specialist
|
||||
"""
|
||||
|
||||
def safe_pct(value):
|
||||
|
|
@ -26,7 +25,6 @@ def build_player_summary(player):
|
|||
sharp = player.get("sharpshooter", {})
|
||||
consistency = player.get("consistency", {})
|
||||
clutch = player.get("clutch", {})
|
||||
support = player.get("support_specialist", {})
|
||||
|
||||
lines = []
|
||||
lines.append(f"{name}\n")
|
||||
|
|
@ -50,9 +48,9 @@ def build_player_summary(player):
|
|||
# Clutch
|
||||
lines.append(f"\nClutch: {safe_tier(clutch.get('tier'))} ({safe_pct(clutch.get('pct')):.1f}%)")
|
||||
lines.append(f" {clutch.get('summary', '')}")
|
||||
|
||||
# Support Specialist
|
||||
lines.append(f"\nSupport Specialist: {safe_tier(support.get('tier'))} ({safe_pct(support.get('pct')):.1f}%)")
|
||||
lines.append(f" {support.get('summary', '')}")
|
||||
|
||||
# Breaker
|
||||
lines.append(f"\nBreaker: {safe_tier(breaker.get('tier'))} ({safe_pct(breaker.get('pct')):.1f}%)")
|
||||
lines.append(f" {breaker.get('summary', '')}")
|
||||
|
||||
return "\n".join(lines)
|
||||
24
analysis/rookie_leaderboard.py
Normal file
24
analysis/rookie_leaderboard.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
def compute_rookie_leaderboard(career_db, tag="slayer", limit=10):
|
||||
rookies = []
|
||||
|
||||
for cid, pdata in career_db["players"].items():
|
||||
maps = pdata["career"].get("maps", 0)
|
||||
if maps >= 10:
|
||||
continue # not a rookie
|
||||
|
||||
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"),
|
||||
})
|
||||
|
||||
rookies.sort(key=lambda r: r["tag_pct"], reverse=True)
|
||||
return rookies[:limit]
|
||||
|
|
@ -24,6 +24,8 @@ def summarize_team_tags(team_players):
|
|||
"sharpshooter",
|
||||
"consistency",
|
||||
"clutch",
|
||||
"anchor",
|
||||
"breaker",
|
||||
]
|
||||
|
||||
summary = {}
|
||||
|
|
@ -68,6 +70,8 @@ def compare_team_strengths(teamA, teamB, team_a_name, team_b_name):
|
|||
"sharpshooter": "Sharpshooter (Accuracy)",
|
||||
"consistency": "Consistency (Stability)",
|
||||
"clutch": "Clutch (High-Pressure Performance)",
|
||||
"anchor": "Anchor (Defensive Stability)",
|
||||
"breaker": "Breaker (Offensive Disruption)",
|
||||
}
|
||||
|
||||
for tag, label in tags.items():
|
||||
|
|
@ -75,7 +79,19 @@ def compare_team_strengths(teamA, teamB, team_a_name, team_b_name):
|
|||
b = teamB[tag]["avg_pct"]
|
||||
|
||||
diff = a - b
|
||||
|
||||
|
||||
|
||||
# Custom storyline for Anchor
|
||||
if tag == "anchor":
|
||||
if abs(diff) < 5:
|
||||
storyline.append("Both teams are evenly matched in defensive stability.")
|
||||
elif diff > 0:
|
||||
storyline.append(f"{team_a_name} have stronger defensive stability, surviving longer and preventing collapses.")
|
||||
else:
|
||||
storyline.append(f"{team_b_name} have stronger defensive stability, giving them an edge in long fights.")
|
||||
continue
|
||||
|
||||
# Generic storyline for all other tags
|
||||
if abs(diff) < 5:
|
||||
storyline.append(f"Both teams are evenly matched in {label}.")
|
||||
elif diff > 0:
|
||||
|
|
|
|||
|
|
@ -1,10 +1,17 @@
|
|||
|
||||
from utils.safe_tag import safe_pct, safe_tier, safe_avg
|
||||
|
||||
# Caster‑friendly display names
|
||||
DISPLAY_NAMES = {
|
||||
"slayer": "Slayer",
|
||||
"objective_payload": "Payload Specialist",
|
||||
"sharpshooter": "Sharpshooter",
|
||||
"consistency": "Consistency",
|
||||
"clutch": "Clutch",
|
||||
"anchor": "Anchor Specialist",
|
||||
"breaker": "Breaker",
|
||||
}
|
||||
|
||||
|
||||
from utils.safe_tag import safe_pct, safe_tier, safe_avg
|
||||
|
||||
def summarize_team_tags(team_players):
|
||||
"""
|
||||
Aggregates tag tiers + percentiles for a team.
|
||||
|
|
@ -14,7 +21,9 @@ def summarize_team_tags(team_players):
|
|||
- tier counts
|
||||
"""
|
||||
|
||||
tags = ["slayer", "objective_payload", "sharpshooter", "consistency", "clutch"]
|
||||
tags = ["slayer", "objective_payload", "sharpshooter",
|
||||
"consistency", "clutch", "anchor", "breaker"]
|
||||
|
||||
summary = {}
|
||||
|
||||
for tag in tags:
|
||||
|
|
@ -24,10 +33,10 @@ def summarize_team_tags(team_players):
|
|||
for p in team_players:
|
||||
tag_data = p.get(tag, {})
|
||||
pcts.append(safe_pct(tag_data.get("pct")))
|
||||
tiers.append(safe_tier(tag_data.get("tier")))
|
||||
tiers.append(safe_tier(tag_data.get("tier"))) # guaranteed valid tier
|
||||
|
||||
summary[tag] = {
|
||||
"avg_pct": safe_avg(pcts),
|
||||
"avg_pct": safe_avg(pcts), # guaranteed numeric
|
||||
"top_tier": max(tiers, key=lambda t: "SABCD".index(t)),
|
||||
"count_S": tiers.count("S"),
|
||||
"count_A": tiers.count("A"),
|
||||
|
|
@ -49,8 +58,9 @@ def classify_team_style(summary):
|
|||
sharpshooter = summary["sharpshooter"]["avg_pct"]
|
||||
consistency = summary["consistency"]["avg_pct"]
|
||||
clutch = summary["clutch"]["avg_pct"]
|
||||
anchor = summary["anchor"]["avg_pct"]
|
||||
|
||||
# Identify primary style
|
||||
# Primary style selection
|
||||
primary = max(
|
||||
{
|
||||
"Slayer-heavy": slayer,
|
||||
|
|
@ -58,11 +68,12 @@ def classify_team_style(summary):
|
|||
"Precision/Sharpshooter": sharpshooter,
|
||||
"Stable/Consistent": consistency,
|
||||
"High-pressure/Clutch": clutch,
|
||||
"Anchor/Support": anchor,
|
||||
}.items(),
|
||||
key=lambda x: x[1]
|
||||
)[0]
|
||||
|
||||
# Identify weaknesses
|
||||
# Weakness detection
|
||||
weaknesses = []
|
||||
if slayer < 40:
|
||||
weaknesses.append("low elimination pressure")
|
||||
|
|
@ -74,6 +85,8 @@ def classify_team_style(summary):
|
|||
weaknesses.append("inconsistent match-to-match output")
|
||||
if clutch < 40:
|
||||
weaknesses.append("poor high-pressure performance")
|
||||
if anchor < 40:
|
||||
weaknesses.append("weak defensive backbone / low anchor stability")
|
||||
|
||||
return primary, weaknesses
|
||||
|
||||
|
|
@ -87,15 +100,16 @@ def generate_team_identity_block(team_players, team_name="Team"):
|
|||
summary = summarize_team_tags(team_players)
|
||||
primary_style, weaknesses = classify_team_style(summary)
|
||||
|
||||
# Build strengths list
|
||||
# Strengths list
|
||||
strengths = []
|
||||
for tag, data in summary.items():
|
||||
if data["avg_pct"] >= 60:
|
||||
display = DISPLAY_NAMES.get(tag, tag.replace("_", " ").title())
|
||||
strengths.append(
|
||||
f"Strong {tag.replace('_', ' ').title()} ({data['top_tier']} Tier)"
|
||||
f"Strong {display} ({data['top_tier']} Tier)"
|
||||
)
|
||||
|
||||
# Build tag breakdown
|
||||
# Tag breakdown
|
||||
tag_block = {}
|
||||
for tag, data in summary.items():
|
||||
tag_block[tag] = {
|
||||
|
|
@ -103,7 +117,6 @@ def generate_team_identity_block(team_players, team_name="Team"):
|
|||
"tier": data["top_tier"],
|
||||
}
|
||||
|
||||
# Return a dict (NOT a string)
|
||||
return {
|
||||
"team_name": team_name,
|
||||
"team_style": primary_style,
|
||||
|
|
@ -111,7 +124,8 @@ def generate_team_identity_block(team_players, team_name="Team"):
|
|||
"weaknesses": weaknesses,
|
||||
"tags": tag_block,
|
||||
}
|
||||
|
||||
|
||||
|
||||
def format_team_identity(identity_data):
|
||||
"""
|
||||
Converts the identity block dict into a readable text block.
|
||||
|
|
@ -125,30 +139,30 @@ def format_team_identity(identity_data):
|
|||
lines.append("")
|
||||
|
||||
# Strengths
|
||||
lines.append("Strengths:")
|
||||
if identity_data["strengths"]:
|
||||
lines.append("Strengths:")
|
||||
for s in identity_data["strengths"]:
|
||||
lines.append(f" - {s}")
|
||||
else:
|
||||
lines.append("Strengths:")
|
||||
lines.append(" - None identified")
|
||||
|
||||
lines.append("")
|
||||
|
||||
# Weaknesses
|
||||
lines.append("Weaknesses:")
|
||||
if identity_data["weaknesses"]:
|
||||
lines.append("Weaknesses:")
|
||||
for w in identity_data["weaknesses"]:
|
||||
lines.append(f" - {w}")
|
||||
else:
|
||||
lines.append("Weaknesses:")
|
||||
lines.append(" - None identified")
|
||||
|
||||
lines.append("")
|
||||
lines.append("Tag Breakdown:")
|
||||
|
||||
for tag, data in identity_data["tags"].items():
|
||||
tag_name = tag.replace("_", " ").title()
|
||||
lines.append(f" - {tag_name}: {data['tier']} Tier (avg {data['avg_pct']:.1f} percentile)")
|
||||
name = DISPLAY_NAMES.get(tag, tag.replace("_", " ").title())
|
||||
lines.append(
|
||||
f" - {name}: {data['tier']} Tier (avg {data['avg_pct']:.1f} percentile)"
|
||||
)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
|
@ -33,7 +33,8 @@ from match_engine import (
|
|||
split_two_columns,
|
||||
compute_slayer_prediction,
|
||||
rank_match_players_consistency_career,
|
||||
rank_match_players_support_specialist,
|
||||
rank_match_players_by_tag,
|
||||
_add_score_fields,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------
|
||||
|
|
@ -42,6 +43,9 @@ from match_engine import (
|
|||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
CONFIG_PATH = os.path.join(BASE_DIR, "config.json")
|
||||
SCREENS_DIR = os.path.join(BASE_DIR, "screens")
|
||||
|
||||
|
||||
|
||||
# Load config (or default)
|
||||
if os.path.exists(CONFIG_PATH):
|
||||
|
|
@ -253,7 +257,46 @@ class DashLeagueGUI:
|
|||
url = f"{API_BASE}/stats?season={self.current_season}"
|
||||
data = fetch_json(url)
|
||||
stats = data.get("data", [])
|
||||
self.stats_by_id = {p["id"]: p for p in stats}
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Load PIR + Career DB
|
||||
# ---------------------------------------------------------
|
||||
from analysis.player_identity_registry import PlayerIdentityRegistry
|
||||
pir = PlayerIdentityRegistry()
|
||||
|
||||
career_path = os.path.join(BASE_DIR, "career_stats.json")
|
||||
with open(career_path, "r", encoding="utf-8") as f:
|
||||
career_db = json.load(f)
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Rebuild stats_by_id keyed by canonical ID
|
||||
# ---------------------------------------------------------
|
||||
new_stats_by_id = {}
|
||||
|
||||
for p in stats:
|
||||
raw_id = p["id"]
|
||||
|
||||
# Always resolve canonical FIRST
|
||||
canonical = pir.resolve(raw_id)
|
||||
p["canonical_id"] = canonical
|
||||
|
||||
# Attach career tags if available
|
||||
pdata = career_db["players"].get(canonical)
|
||||
if pdata:
|
||||
p["slayer"] = pdata.get("slayer", {})
|
||||
p["sharpshooter"] = pdata.get("sharpshooter", {})
|
||||
p["objective_payload"] = pdata.get("objective_payload", {})
|
||||
p["objective_domination"] = pdata.get("objective_domination", {})
|
||||
p["consistency"] = pdata.get("consistency", {})
|
||||
p["clutch"] = pdata.get("clutch", {})
|
||||
p["anchor"] = pdata.get("anchor", {})
|
||||
p["breaker"] = pdata.get("breaker", {})
|
||||
|
||||
# Store under canonical ID
|
||||
new_stats_by_id[canonical] = p
|
||||
|
||||
self.stats_by_id = new_stats_by_id
|
||||
|
||||
self.set_status("Stats synced.")
|
||||
messagebox.showinfo("Success", f"Synced {len(stats)} player stats.")
|
||||
except Exception as e:
|
||||
|
|
@ -276,7 +319,10 @@ class DashLeagueGUI:
|
|||
|
||||
def on_team_select(self, side):
|
||||
team_name = self.team_a_var.get() if side == "A" else self.team_b_var.get()
|
||||
team_players = [p for p in self.stats_by_id.values() if p.get("team") == team_name]
|
||||
team_players = [
|
||||
p for p in self.stats_by_id.values()
|
||||
if p.get("team") == team_name
|
||||
]
|
||||
|
||||
if side == "A":
|
||||
self.team_a_players = team_players
|
||||
|
|
@ -346,6 +392,7 @@ class DashLeagueGUI:
|
|||
# Debug
|
||||
if self.current_match_players:
|
||||
print("DEBUG MATCH PLAYER:", self.current_match_players[0])
|
||||
print("DEBUG PLAYER KEYS:", list(self.current_match_players[0].keys()))
|
||||
else:
|
||||
print("DEBUG MATCH PLAYER: EMPTY LIST")
|
||||
|
||||
|
|
@ -358,34 +405,51 @@ class DashLeagueGUI:
|
|||
with open(career_path, "r", encoding="utf-8") as f:
|
||||
career_db = json.load(f)
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Resolve canonical IDs for all match players
|
||||
# ---------------------------------------------------------
|
||||
from analysis.player_identity_registry import PlayerIdentityRegistry
|
||||
pir = PlayerIdentityRegistry()
|
||||
|
||||
for p in all_players:
|
||||
raw_uuid = (
|
||||
p.get("id")
|
||||
or p.get("PlayerUUID")
|
||||
or p.get("uuid")
|
||||
)
|
||||
if raw_uuid:
|
||||
canonical = pir.resolve(raw_uuid)
|
||||
p["canonical_id"] = canonical
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# 4. Attach modern career tags
|
||||
# -----------------------------
|
||||
if career_db is not None:
|
||||
for p in all_players:
|
||||
pid = (
|
||||
p.get("id")
|
||||
or p.get("PlayerUUID")
|
||||
or p.get("uuid")
|
||||
)
|
||||
canonical = p.get("canonical_id")
|
||||
if not canonical:
|
||||
continue
|
||||
|
||||
if pid and pid in career_db["players"]:
|
||||
pdata = career_db["players"][pid]
|
||||
pdata = career_db["players"].get(canonical)
|
||||
if not pdata:
|
||||
continue
|
||||
|
||||
# Attach modern tags directly
|
||||
p["slayer"] = pdata.get("slayer", {})
|
||||
p["sharpshooter"] = pdata.get("sharpshooter", {})
|
||||
p["objective_payload"] = pdata.get("objective_payload", {})
|
||||
# Core tags
|
||||
p["slayer"] = pdata.get("slayer", {})
|
||||
p["sharpshooter"] = pdata.get("sharpshooter", {})
|
||||
p["objective_payload"] = pdata.get("objective_payload", {})
|
||||
|
||||
# DO NOT overwrite match-based DomObj
|
||||
if "objective_domination" not in p:
|
||||
p["objective_domination"] = pdata.get("objective_domination", {})
|
||||
# Do NOT overwrite match-based DomObj
|
||||
if "objective_domination" not in p:
|
||||
p["objective_domination"] = pdata.get("objective_domination", {})
|
||||
|
||||
p["consistency"] = pdata.get("consistency", {})
|
||||
p["clutch"] = pdata.get("clutch", {})
|
||||
|
||||
# Support Specialist
|
||||
p["support_specialist"] = pdata.get("support_specialist", {})
|
||||
# Career-based tags
|
||||
p["consistency"] = pdata.get("consistency", {})
|
||||
p["clutch"] = pdata.get("clutch", {})
|
||||
p["anchor"] = pdata.get("anchor", {})
|
||||
p["breaker"] = pdata.get("breaker", {})
|
||||
|
||||
|
||||
# -----------------------------
|
||||
|
|
@ -425,6 +489,35 @@ class DashLeagueGUI:
|
|||
write_text(os.path.join(slot_dir, "Captures.txt"), p.get("DOM_Captures", ""))
|
||||
write_text(os.path.join(slot_dir, "Counters.txt"), p.get("DOM_Counters", ""))
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# 6. Export Rookie Leaderboard
|
||||
# -----------------------------
|
||||
from analysis.rookie_leaderboard import compute_rookie_leaderboard
|
||||
|
||||
rookies = compute_rookie_leaderboard(career_db, tag="slayer") # default tag
|
||||
|
||||
rookie_dir = os.path.join(SCREENS_DIR, "rookies")
|
||||
os.makedirs(rookie_dir, exist_ok=True)
|
||||
|
||||
# Full table
|
||||
table_lines = []
|
||||
for i, r in enumerate(rookies):
|
||||
table_lines.append(
|
||||
f"{i+1}. {r['name']} — {r['tag_tier']} Tier ({r['tag_pct']:.1f}%) "
|
||||
f"KD {r['kd']:.2f} — {r['kills']} Kills — {r['score']} Score"
|
||||
)
|
||||
write_text(os.path.join(rookie_dir, "table.txt"), "\n".join(table_lines))
|
||||
|
||||
# Individual rows
|
||||
for i, r in enumerate(rookies):
|
||||
row = (
|
||||
f"{i+1}. {r['name']} — {r['tag_tier']} Tier ({r['tag_pct']:.1f}%)\n"
|
||||
f"KD {r['kd']:.2f} — {r['kills']} Kills — {r['score']} Score"
|
||||
)
|
||||
write_text(os.path.join(rookie_dir, f"p{i}.txt"), row)
|
||||
|
||||
|
||||
self.set_status("Player slots generated.")
|
||||
messagebox.showinfo("Success", "Player slots generated into p0–p9.")
|
||||
|
||||
|
|
@ -442,6 +535,8 @@ class DashLeagueGUI:
|
|||
output_path = os.path.join(BASE_DIR, "career_stats.json")
|
||||
|
||||
build_career_database(output_path)
|
||||
#print("DEBUG CAREER DB KEYS:", career_db.keys())
|
||||
#print("DEBUG PLAYER COUNT:", len(career_db.get("players", {})))
|
||||
|
||||
self.set_status("Career database built successfully.")
|
||||
messagebox.showinfo("Success", "Career database has been built.")
|
||||
|
|
@ -592,15 +687,6 @@ Consistency Summary:
|
|||
if isinstance(score, (int, float)):
|
||||
tags_out.append(f"Cons {score:.2f}")
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Support Specialist (career)
|
||||
# ---------------------------------------------------------
|
||||
support = player_entry.get("support_specialist", {})
|
||||
if isinstance(support, dict):
|
||||
score = support.get("raw")
|
||||
if isinstance(score, (int, float)):
|
||||
tags_out.append(f"Supp {score:.2f}")
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Payload Objective Specialist (match-based)
|
||||
# ---------------------------------------------------------
|
||||
|
|
@ -636,8 +722,8 @@ Consistency Summary:
|
|||
return
|
||||
|
||||
# Extract players for each team
|
||||
teamA_players = [self.stats_by_id[p["id"]] for p in self.current_match_players if p.get("team") == team_a]
|
||||
teamB_players = [self.stats_by_id[p["id"]] for p in self.current_match_players if p.get("team") == team_b]
|
||||
teamA_players = [self.stats_by_id[p["canonical_id"]] for p in self.current_match_players]
|
||||
teamB_players = [self.stats_by_id[p["canonical_id"]] for p in self.current_match_players]
|
||||
|
||||
#print("TEAM A SAMPLE:", teamA_players[0])
|
||||
#print("TEAM B SAMPLE:", teamB_players[0])
|
||||
|
|
@ -718,7 +804,8 @@ Consistency Summary:
|
|||
"Domination Objective Specialist",
|
||||
"Consistency",
|
||||
"Clutch",
|
||||
"Support Specialist",
|
||||
"Anchor",
|
||||
"Breaker",
|
||||
]
|
||||
|
||||
ttk.Label(win, text="Select Tag:").pack(anchor="w", padx=10, pady=5)
|
||||
|
|
@ -743,23 +830,34 @@ Consistency Summary:
|
|||
if tag == "Slayer":
|
||||
ranked = rank_match_players_slayer(self.current_match_players)
|
||||
|
||||
elif tag == "Sharpshooter":
|
||||
ranked = rank_match_players_sharpshooter(self.current_match_players)
|
||||
|
||||
elif tag == "Payload Objective Specialist":
|
||||
ranked = rank_match_players_objpl(self.current_match_players)
|
||||
|
||||
elif tag == "Domination Objective Specialist":
|
||||
ranked = rank_match_players_objdom(self.current_match_players)
|
||||
|
||||
elif tag == "Clutch":
|
||||
ranked = rank_match_players_clutch(self.current_match_players)
|
||||
|
||||
elif tag == "Consistency":
|
||||
ranked = rank_match_players_consistency_career(self.current_match_players)
|
||||
|
||||
elif tag == "Support Specialist":
|
||||
ranked = rank_match_players_support_specialist(self.current_match_players)
|
||||
|
||||
else: # Sharpshooter
|
||||
ranked = rank_match_players_sharpshooter(self.current_match_players)
|
||||
|
||||
elif tag == "Clutch":
|
||||
ranked = rank_match_players_clutch(self.current_match_players)
|
||||
|
||||
elif tag == "Anchor":
|
||||
ranked = _add_score_fields(
|
||||
rank_match_players_by_tag(self.current_match_players, "anchor")
|
||||
)
|
||||
|
||||
elif tag == "Breaker":
|
||||
ranked = _add_score_fields(
|
||||
rank_match_players_by_tag(self.current_match_players, "breaker")
|
||||
)
|
||||
|
||||
else:
|
||||
messagebox.showerror("Error", f"Unknown tag: {tag}")
|
||||
return
|
||||
|
||||
if not ranked:
|
||||
output.delete("1.0", tk.END)
|
||||
|
|
@ -900,7 +998,25 @@ Consistency Summary:
|
|||
text += "Where:\n"
|
||||
text += "- Pressure Efficiency = Damage / (Deaths + 1)\n"
|
||||
text += "- Collapse Avoidance = Player's lowest per-map performance score\n"
|
||||
text += "- Conversion Rate = (Accuracy × 0.50) + (Headshot Rate × 0.50)\n"
|
||||
text += "- Conversion Rate = (Accuracy × 0.50) + (Headshot Rate × 0.50)\n"
|
||||
|
||||
elif tag == "Anchor":
|
||||
text += "\nAnchor Formula:\n"
|
||||
text += "Anchor = (Survivability × 0.45) + (DefensivePresence × 0.30) + (Consistency × 0.25)\n"
|
||||
text += "Where:\n"
|
||||
text += "- Survivability = (LeagueAvgDeathsPerMap / PlayerDeathsPerMap), clamped to [0.25, 1.50] then normalized to 0–1\n"
|
||||
text += "- DefensivePresence = (DamagePerMap / LeagueAvgDamagePerMap), clamped to [0, 1.5] then normalized to 0–1\n"
|
||||
text += "- Consistency = normalized 0–1 stability score from the Consistency tag\n"
|
||||
text += "Survivability reflects how hard the player is to remove; Defensive Presence measures baseline contribution; Consistency rewards stable defensive performance.\n"
|
||||
|
||||
elif tag == "Breaker":
|
||||
text += "\nBreaker Formula:\n"
|
||||
text += "Breaker = (DamageNorm × 0.60) + (KillsNorm × 0.40)\n"
|
||||
text += "Where:\n"
|
||||
text += "- DamageNorm = Damage per map normalized against league average\n"
|
||||
text += "- KillsNorm = Kills per map normalized against league average\n"
|
||||
text += "Breaker measures disruptive offensive pressure through high damage output and aggressive fragging.\n"
|
||||
|
||||
|
||||
output.delete("1.0", tk.END)
|
||||
output.insert(tk.END, text)
|
||||
|
|
|
|||
|
|
@ -9,15 +9,18 @@ from tags.objective_domination import compute_dom_objective_for_career_player
|
|||
from tags.sharpshooter import compute_sharpshooter_for_career_player
|
||||
from tags.consistency import build_consistency_tag
|
||||
from tags.clutch import compute_clutch, compute_clutch_raw
|
||||
from tags.support_specialist import build_support_specialist_tag
|
||||
from tags.anchor import compute_anchor_for_career_player
|
||||
from tags.breaker import compute_breaker_raw
|
||||
|
||||
from data.league_metrics import compute_league_metrics, compute_league_clutch_metrics
|
||||
from data.tag_framework import percentile_rank, tier_from_percentile
|
||||
|
||||
|
||||
|
||||
|
||||
def build_career_database(output_path):
|
||||
|
||||
print("USING DB:", db_access.DB_PATH)
|
||||
#print("USING DB:", db_access.DB_PATH)
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
|
|
@ -29,6 +32,13 @@ def build_career_database(output_path):
|
|||
LEFT JOIN players p ON p.PlayerUUID = s.PlayerUUID;
|
||||
""")
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Load Player Identity Registry (PIR)
|
||||
# ---------------------------------------------------------
|
||||
from analysis.player_identity_registry import PlayerIdentityRegistry
|
||||
pir = PlayerIdentityRegistry()
|
||||
|
||||
|
||||
if not player_rows:
|
||||
print("No players found in local DB.")
|
||||
return False
|
||||
|
|
@ -39,12 +49,20 @@ def build_career_database(output_path):
|
|||
# 2. Build per-player career stats from local DB
|
||||
# ---------------------------------------------------------
|
||||
for row in player_rows:
|
||||
pid = row["PlayerUUID"]
|
||||
raw_uuid = str(row["PlayerUUID"])
|
||||
name = row["PlayerGameName"] or "Unknown"
|
||||
|
||||
matches = db_access.get_player_match_history(pid)
|
||||
# Resolve canonical identity
|
||||
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)
|
||||
if not matches:
|
||||
continue
|
||||
|
||||
# Use CANONICAL ID as the key in the career DB
|
||||
pid = canonical_id
|
||||
|
||||
#print("DEBUG MATCH ROW FOR", pid, ":", matches[0])
|
||||
#break
|
||||
|
|
@ -175,19 +193,21 @@ def build_career_database(output_path):
|
|||
|
||||
final_db["league_averages"] = league_averages
|
||||
|
||||
|
||||
from tags.support_specialist import compute_support_specialist # at top of file if not already
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 4.6 Build Breaker distribution
|
||||
# ---------------------------------------------------------
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 4.5 Build Support Specialist distribution
|
||||
# ---------------------------------------------------------
|
||||
support_distribution = []
|
||||
|
||||
breaker_distribution = []
|
||||
for pid, pdata in final_db["players"].items():
|
||||
raw_support = compute_support_specialist(pdata, league_averages)
|
||||
support_distribution.append(raw_support)
|
||||
career = pdata["career"]
|
||||
damage_per_map = career["damage"] / max(1, career["maps"])
|
||||
kills_per_map = career["kills"] / max(1, career["maps"])
|
||||
|
||||
# Write back normalized career block
|
||||
pdata["career"] = career
|
||||
raw_breaker = compute_breaker_raw(damage_per_map, kills_per_map)
|
||||
breaker_distribution.append(raw_breaker)
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
|
|
@ -241,6 +261,15 @@ def build_career_database(output_path):
|
|||
"raw": consistency_result.normalized_score,
|
||||
"summary": consistency_result.summary_short
|
||||
}
|
||||
|
||||
# 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(
|
||||
|
|
@ -248,13 +277,24 @@ def build_career_database(output_path):
|
|||
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"])
|
||||
|
||||
# Support Specialist
|
||||
pdata["support_specialist"] = build_support_specialist_tag(
|
||||
pdata,
|
||||
league_averages,
|
||||
support_distribution
|
||||
)
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -43,7 +43,8 @@ def compute_league_metrics(all_players):
|
|||
"payload": [],
|
||||
"consistency": [],
|
||||
"domination": [],
|
||||
"support_specialist": [],
|
||||
"anchor": [],
|
||||
"breaker": [],
|
||||
}
|
||||
|
||||
# -----------------------------------------
|
||||
|
|
@ -88,6 +89,9 @@ def compute_league_metrics(all_players):
|
|||
key: (sum(values) / max(1, len(values)))
|
||||
for key, values in acc.items()
|
||||
}
|
||||
|
||||
# Alias for clarity: deaths is already per-map
|
||||
league_averages["deaths_per_map"] = league_averages.get("deaths", 0.0)
|
||||
|
||||
# Minimum smoothing to avoid divide-by-zero explosions
|
||||
league_averages["accuracy"] = max(league_averages["accuracy"], 0.05)
|
||||
|
|
@ -112,11 +116,16 @@ def compute_league_metrics(all_players):
|
|||
# Domination
|
||||
dist["domination"].append(compute_dom_raw(p))
|
||||
|
||||
from tags.support_specialist import compute_support_specialist
|
||||
from tags.anchor import compute_anchor_raw
|
||||
|
||||
dist["support_specialist"].append(
|
||||
compute_support_specialist(p, league_averages)
|
||||
# Consistency raw score for distribution building
|
||||
# (You already compute consistency later, but for distributions we only need the raw normalized score)
|
||||
consistency_norm = p.get("consistency", {}).get("raw", 0.5)
|
||||
|
||||
dist["anchor"].append(
|
||||
compute_anchor_raw(p, league_averages, consistency_norm)
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------
|
||||
# Sort distributions
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
|
||||
|
||||
import bisect
|
||||
from tags.support_specialist import build_support_specialist_tag
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,20 @@ def load_career_db():
|
|||
def _get_player_entry(db, pid):
|
||||
pid = str(pid)
|
||||
return db["players"].get(pid)
|
||||
|
||||
|
||||
def attach_career_tags_to_match_player(match_player, career_entry):
|
||||
"""
|
||||
Injects career tag blocks into a match_player object so that
|
||||
storylines, predictions, and UI panels can access:
|
||||
- consistency
|
||||
- anchor
|
||||
- clutch
|
||||
- breaker
|
||||
"""
|
||||
for tag in ("consistency", "anchor", "clutch", "breaker"):
|
||||
if tag in career_entry:
|
||||
match_player[tag] = career_entry[tag]
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
|
|
@ -47,6 +61,8 @@ def rank_match_players_by_tag(match_players, tag_name):
|
|||
entry = _get_player_entry(db, pid)
|
||||
if not entry:
|
||||
continue
|
||||
|
||||
attach_career_tags_to_match_player(p, entry)
|
||||
|
||||
tag = entry.get(tag_name, {})
|
||||
pct = tag.get("pct", 0)
|
||||
|
|
@ -233,37 +249,4 @@ def compute_slayer_prediction(team_a_players, team_b_players):
|
|||
"teamB_win": round(pB * 100),
|
||||
}
|
||||
|
||||
|
||||
def rank_match_players_support_specialist(players):
|
||||
"""
|
||||
Ranks players by Support Specialist (career tag) using percentile.
|
||||
"""
|
||||
|
||||
ranked = []
|
||||
|
||||
for p in players:
|
||||
tag = p.get("support_specialist", {})
|
||||
|
||||
# Use percentile as the ranking basis
|
||||
pct = tag.get("pct")
|
||||
if pct is None:
|
||||
continue
|
||||
|
||||
ranked.append({
|
||||
"name": p.get("name", "Unknown"),
|
||||
"team": p.get("team", ""),
|
||||
"pct": pct,
|
||||
"tier": tag.get("tier", "D"),
|
||||
"summary": tag.get("summary", ""),
|
||||
"score_raw": pct, # used for team totals
|
||||
"score_display": f"{pct:.1f}", # what you see in the table
|
||||
})
|
||||
|
||||
# Sort descending by percentile
|
||||
ranked.sort(key=lambda x: x["pct"], reverse=True)
|
||||
|
||||
# Assign ranks
|
||||
for i, entry in enumerate(ranked, start=1):
|
||||
entry["rank"] = i
|
||||
|
||||
return ranked
|
||||
|
||||
37
rankings.py
37
rankings.py
|
|
@ -109,37 +109,32 @@ def rank_match_players_clutch(players):
|
|||
return ranked
|
||||
|
||||
|
||||
def rank_match_players_support_specialist(players):
|
||||
"""
|
||||
Ranks players by Support Specialist (career tag).
|
||||
Uses the already-computed tag stored in each player's data.
|
||||
"""
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Generic Match Tag Ranking (Modern Tag System)
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def rank_match_players_by_tag(players, tag_name):
|
||||
ranked = []
|
||||
|
||||
for p in players:
|
||||
tag = p.get("support_specialist", {})
|
||||
raw = tag.get("raw")
|
||||
tag = p.get(tag_name, {})
|
||||
|
||||
# Skip players with no data
|
||||
if raw is None:
|
||||
continue
|
||||
# Modern tags store raw score under "raw"
|
||||
score = 0.0
|
||||
if isinstance(tag, dict):
|
||||
score = tag.get("raw", 0.0)
|
||||
|
||||
ranked.append({
|
||||
"name": p.get("name", "Unknown"),
|
||||
"team": p.get("team", ""),
|
||||
"raw": raw,
|
||||
"pct": tag.get("pct", 0.0),
|
||||
"tier": tag.get("tier", "D"),
|
||||
"summary": tag.get("summary", ""),
|
||||
"score_display": f"{raw:.2f}",
|
||||
"score_raw": score,
|
||||
"score_display": f"{score:.2f}",
|
||||
})
|
||||
|
||||
# Sort descending by raw score
|
||||
ranked.sort(key=lambda x: x["raw"], reverse=True)
|
||||
ranked.sort(key=lambda x: x["score_raw"], reverse=True)
|
||||
|
||||
# Assign ranks
|
||||
for i, entry in enumerate(ranked, start=1):
|
||||
entry["rank"] = i
|
||||
for i, r in enumerate(ranked, start=1):
|
||||
r["rank"] = i
|
||||
|
||||
return ranked
|
||||
return ranked
|
||||
115
tags/anchor.py
Normal file
115
tags/anchor.py
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
# ---------------------------------------------------------
|
||||
# Anchor Tag — Defensive Stability & Survivability
|
||||
# Mode-agnostic, uses only universal stats
|
||||
# ---------------------------------------------------------
|
||||
|
||||
from tags.tag_framework import build_tag_output
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Anchor Summary (caster-friendly)
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def anchor_summary(tier, pct):
|
||||
if tier == "S":
|
||||
return f"Elite defensive anchor — top {100 - pct:.0f}% in the league for survivability and stability."
|
||||
if tier == "A":
|
||||
return "High-impact anchor with strong survivability and consistent defensive presence."
|
||||
if tier == "B":
|
||||
return "Above-average anchor who provides reliable defensive value."
|
||||
if tier == "C":
|
||||
return "Below-average anchor; inconsistent survivability or defensive presence."
|
||||
return "Struggles to provide defensive stability."
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Survivability Normalization
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def compute_survivability_norm(deaths_per_map, league_avg_deaths):
|
||||
"""
|
||||
Survivability = league_avg / player_deaths
|
||||
Clamped to [0.25, 1.50], then normalized to 0–1.
|
||||
"""
|
||||
if deaths_per_map <= 0:
|
||||
# Extremely rare, but treat as max survivability
|
||||
return 1.0
|
||||
|
||||
raw = league_avg_deaths / deaths_per_map
|
||||
|
||||
# Clamp to avoid extreme values
|
||||
raw = max(0.25, min(1.50, raw))
|
||||
|
||||
# Normalize to 0–1
|
||||
return (raw - 0.25) / (1.50 - 0.25)
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Defensive Presence Normalization
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def compute_defensive_presence_norm(damage_per_map, league_avg_damage):
|
||||
"""
|
||||
Defensive presence uses damage as a universal proxy.
|
||||
Normalized to league average, clamped to [0, 1.5], then normalized to 0–1.
|
||||
"""
|
||||
if league_avg_damage <= 0:
|
||||
return 0.0
|
||||
|
||||
raw = damage_per_map / league_avg_damage
|
||||
|
||||
# Clamp
|
||||
raw = max(0.0, min(1.5, raw))
|
||||
|
||||
# Normalize to 0–1
|
||||
return raw / 1.5
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Anchor Raw Score
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def compute_anchor_raw(pdata, league_averages, consistency_norm):
|
||||
"""
|
||||
pdata: player stats dict (career-level)
|
||||
league_averages: dict with league-wide averages
|
||||
consistency_norm: 0–1 normalized consistency score
|
||||
"""
|
||||
|
||||
c = pdata["career"]
|
||||
|
||||
deaths = c["deaths"]
|
||||
damage = c["damage"]
|
||||
maps = max(1, c["maps"])
|
||||
|
||||
deaths_per_map = deaths / maps
|
||||
damage_per_map = damage / maps
|
||||
|
||||
league_avg_deaths = league_averages.get("deaths_per_map", 1)
|
||||
league_avg_damage = league_averages.get("damage", 1)
|
||||
|
||||
survivability = compute_survivability_norm(deaths_per_map, league_avg_deaths)
|
||||
defensive_presence = compute_defensive_presence_norm(damage_per_map, league_avg_damage)
|
||||
|
||||
# Weighted anchor score
|
||||
raw = (
|
||||
survivability * 0.45 +
|
||||
defensive_presence * 0.30 +
|
||||
consistency_norm * 0.25
|
||||
)
|
||||
|
||||
return max(0.0, min(1.0, raw))
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Public API
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def compute_anchor_for_career_player(pdata, league_averages, anchor_distribution, consistency_norm):
|
||||
"""
|
||||
pdata: player stats
|
||||
league_averages: league-wide averages
|
||||
anchor_distribution: distribution of raw anchor scores for percentile mapping
|
||||
consistency_norm: 0–1 normalized consistency score (from consistency tag)
|
||||
"""
|
||||
raw = compute_anchor_raw(pdata, league_averages, consistency_norm)
|
||||
return build_tag_output(raw, anchor_distribution, anchor_summary)
|
||||
37
tags/breaker.py
Normal file
37
tags/breaker.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# ============================================================
|
||||
# BREAKER TAG (v1 - Mode Safe)
|
||||
# ------------------------------------------------------------
|
||||
# Breaker measures offensive disruption using only stats that
|
||||
# exist for BOTH Payload and Domination:
|
||||
#
|
||||
# - Damage per map
|
||||
# - Kills per map
|
||||
#
|
||||
# No captures, no counters, no streaks, no timing.
|
||||
# ============================================================
|
||||
|
||||
def compute_breaker_raw(damage_per_map, kills_per_map):
|
||||
"""
|
||||
Computes the raw Breaker score (0–infinity).
|
||||
This is later normalized by league distribution.
|
||||
|
||||
Breaker v1:
|
||||
60% damage pressure
|
||||
40% kill pressure
|
||||
"""
|
||||
return (0.60 * damage_per_map) + (0.40 * kills_per_map)
|
||||
|
||||
|
||||
def build_breaker_summary(tier, pct):
|
||||
"""
|
||||
Returns caster-friendly summary text.
|
||||
"""
|
||||
if tier == "S":
|
||||
return "One of the league’s top offensive disruptors."
|
||||
if tier == "A":
|
||||
return "A strong pressure player who cracks open defenses."
|
||||
if tier == "B":
|
||||
return "Provides steady offensive pressure."
|
||||
if tier == "C":
|
||||
return "Occasional pressure but inconsistent impact."
|
||||
return "Low offensive disruption output."
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
from utils.safe_tag import safe_raw
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# RAW SCORE CALCULATION
|
||||
# ---------------------------------------------------------
|
||||
|
||||
|
||||
def compute_support_specialist(player, league_averages):
|
||||
c = player.get("career", {})
|
||||
|
||||
dmg = safe_raw(c.get("damage"))
|
||||
deaths = safe_raw(c.get("deaths"))
|
||||
push_time = safe_raw(c.get("push_time"))
|
||||
dom_caps = safe_raw(c.get("captures"))
|
||||
dom_counters = safe_raw(c.get("counters"))
|
||||
|
||||
# League anchors
|
||||
league_push = max(1.0, league_averages.get("push_time", 1.0))
|
||||
league_dom_actions = max(1.0, league_averages.get("dom_actions", 1.0))
|
||||
league_damage = max(1.0, league_averages.get("damage", 1.0))
|
||||
league_deaths = max(1.0, league_averages.get("deaths", 1.0))
|
||||
league_pressure = max(1e-6, league_averages.get("pressure_eff", 1.0))
|
||||
|
||||
# Objective presence (relative to league, capped)
|
||||
presence_payload = (push_time / league_push)
|
||||
presence_dom = ((dom_caps + dom_counters) / league_dom_actions)
|
||||
objective_presence = max(presence_payload, presence_dom)
|
||||
objective_presence = max(0.0, min(objective_presence, 2.0))
|
||||
|
||||
# Survivability: pressure vs league pressure_eff
|
||||
player_pressure = dmg / max(1.0, deaths)
|
||||
survivability = player_pressure / league_pressure
|
||||
survivability = max(0.0, min(survivability, 2.0))
|
||||
|
||||
# Damage component: relative to league damage
|
||||
damage_component = dmg / league_damage
|
||||
damage_component = max(0.0, min(damage_component, 2.0))
|
||||
|
||||
# Low-death bonus: 1 is good, 0 is bad
|
||||
low_death_bonus = 1.0 - (deaths / league_deaths)
|
||||
low_death_bonus = max(0.0, min(low_death_bonus, 1.0))
|
||||
|
||||
# Weighted sum, then scale back into 0–1
|
||||
raw = (
|
||||
0.35 * objective_presence +
|
||||
0.35 * survivability +
|
||||
0.20 * damage_component +
|
||||
0.10 * low_death_bonus
|
||||
) / 2.0 # max of the capped components is 2 → divide by 2 to keep ≤ 1
|
||||
|
||||
return raw
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# TAG BUILDER (RAW → PERCENTILE → TIER → SUMMARY)
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def build_support_specialist_tag(player, league_averages, distribution):
|
||||
"""
|
||||
Converts raw Support Specialist score into full tag output.
|
||||
"""
|
||||
|
||||
from tags.tag_framework import build_tag_output
|
||||
|
||||
raw = compute_support_specialist(player, league_averages)
|
||||
|
||||
def summary_fn(tier, pct):
|
||||
if pct >= 85:
|
||||
return "Elite support presence — stabilizes fights and enables strong objective pushes."
|
||||
if pct >= 70:
|
||||
return "Reliable support player with strong survivability and objective presence."
|
||||
if pct >= 50:
|
||||
return "Provides steady support value through survivability and objective actions."
|
||||
if pct >= 30:
|
||||
return "Occasional support impact but inconsistent objective presence."
|
||||
return "Limited support impact — low survivability and minimal objective presence."
|
||||
|
||||
return build_tag_output(raw, distribution, summary_fn)
|
||||
Loading…
Reference in a new issue