Support Specialist v1: per-map league metrics + normalized stats
This commit is contained in:
parent
3dc737d819
commit
ef88da9c53
13 changed files with 438 additions and 65 deletions
|
|
@ -5,6 +5,12 @@ from analysis.team_identity_v2 import format_team_identity, generate_team_identi
|
|||
from analysis.storylines import generate_storyline
|
||||
from analysis.predictions_v2 import generate_prediction
|
||||
|
||||
import re
|
||||
|
||||
def safe_filename(name):
|
||||
# Remove all illegal Windows filename characters
|
||||
return re.sub(r'[\\/:*?"<>|]', '', name)
|
||||
|
||||
|
||||
def export_obs_player_summaries(output_folder, team_players):
|
||||
"""
|
||||
|
|
@ -15,7 +21,7 @@ def export_obs_player_summaries(output_folder, team_players):
|
|||
|
||||
for p in team_players:
|
||||
name = p.get("name", "Unknown")
|
||||
safe_name = name.replace(" ", "_")
|
||||
safe_name = safe_filename(name)
|
||||
|
||||
path = os.path.join(output_folder, f"{safe_name}_summary.txt")
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
|
|
@ -29,7 +35,8 @@ def export_obs_team_identity(output_folder, team_players, team_name):
|
|||
|
||||
os.makedirs(output_folder, exist_ok=True)
|
||||
|
||||
path = os.path.join(output_folder, f"{team_name}_identity.txt")
|
||||
safe_team = safe_filename(team_name)
|
||||
path = os.path.join(output_folder, f"{safe_team}_identity.txt")
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
identity_data = generate_team_identity_block(team_players, team_name)
|
||||
identity_text = format_team_identity(identity_data)
|
||||
|
|
|
|||
|
|
@ -7,8 +7,18 @@ def build_player_summary(player):
|
|||
- Sharpshooter
|
||||
- Consistency
|
||||
- Clutch
|
||||
- Support Specialist
|
||||
"""
|
||||
|
||||
def safe_pct(value):
|
||||
try:
|
||||
return float(value) if value is not None else 0.0
|
||||
except:
|
||||
return 0.0
|
||||
|
||||
def safe_tier(value):
|
||||
return value if value not in (None, "", "None") else "D"
|
||||
|
||||
name = player.get("name", "Unknown Player")
|
||||
|
||||
slayer = player.get("slayer", {})
|
||||
|
|
@ -16,28 +26,33 @@ 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")
|
||||
|
||||
# Slayer
|
||||
lines.append(f"Slayer: {slayer.get('tier', 'D')} ({slayer.get('pct', 0):.1f}%)")
|
||||
lines.append(f"Slayer: {safe_tier(slayer.get('tier'))} ({safe_pct(slayer.get('pct')):.1f}%)")
|
||||
lines.append(f" {slayer.get('summary', '')}")
|
||||
|
||||
# Payload
|
||||
lines.append(f"\nPayload Objective: {payload.get('tier', 'D')} ({payload.get('pct', 0):.1f}%)")
|
||||
lines.append(f"\nPayload Objective: {safe_tier(payload.get('tier'))} ({safe_pct(payload.get('pct')):.1f}%)")
|
||||
lines.append(f" {payload.get('summary', '')}")
|
||||
|
||||
# Sharpshooter
|
||||
lines.append(f"\nSharpshooter: {sharp.get('tier', 'D')} ({sharp.get('pct', 0):.1f}%)")
|
||||
lines.append(f"\nSharpshooter: {safe_tier(sharp.get('tier'))} ({safe_pct(sharp.get('pct')):.1f}%)")
|
||||
lines.append(f" {sharp.get('summary', '')}")
|
||||
|
||||
# Consistency
|
||||
lines.append(f"\nConsistency: {consistency.get('tier', 'D')} ({consistency.get('pct', 0):.1f}%)")
|
||||
lines.append(f"\nConsistency: {safe_tier(consistency.get('tier'))} ({safe_pct(consistency.get('pct')):.1f}%)")
|
||||
lines.append(f" {consistency.get('summary', '')}")
|
||||
|
||||
# Clutch
|
||||
lines.append(f"\nClutch: {clutch.get('tier', 'D')} ({clutch.get('pct', 0):.1f}%)")
|
||||
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', '')}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
|
@ -1,23 +1,16 @@
|
|||
|
||||
|
||||
from analysis.team_identity_v2 import summarize_team_tags, classify_team_style
|
||||
from utils.safe_tag import safe_pct, safe_avg, safe_tier
|
||||
|
||||
|
||||
def compute_team_power_score(summary):
|
||||
"""
|
||||
Converts team tag percentiles into a single weighted power score.
|
||||
Weights reflect real match impact:
|
||||
Slayer: 30%
|
||||
Objective: 25%
|
||||
Consistency: 20%
|
||||
Clutch: 15%
|
||||
Sharpshooter: 10%
|
||||
"""
|
||||
|
||||
return (
|
||||
summary["slayer"]["avg_pct"] * 0.30 +
|
||||
summary["objective_payload"]["avg_pct"] * 0.25 +
|
||||
summary["consistency"]["avg_pct"] * 0.20 +
|
||||
summary["clutch"]["avg_pct"] * 0.15 +
|
||||
summary["sharpshooter"]["avg_pct"] * 0.10
|
||||
safe_pct(summary["slayer"]["avg_pct"]) * 0.30 +
|
||||
safe_pct(summary["objective_payload"]["avg_pct"]) * 0.25 +
|
||||
safe_pct(summary["consistency"]["avg_pct"]) * 0.20 +
|
||||
safe_pct(summary["clutch"]["avg_pct"]) * 0.15 +
|
||||
safe_pct(summary["sharpshooter"]["avg_pct"]) * 0.10
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ __all__ = ["matchup_storyline", "generate_storyline"]
|
|||
# Team Tag Aggregation
|
||||
# ---------------------------------------------------------
|
||||
|
||||
from utils.safe_tag import safe_pct, safe_tier, safe_avg
|
||||
|
||||
def summarize_team_tags(team_players):
|
||||
"""
|
||||
Aggregates tag tiers + percentiles for a team.
|
||||
|
|
@ -32,23 +34,11 @@ def summarize_team_tags(team_players):
|
|||
|
||||
for p in team_players:
|
||||
tag_data = p.get(tag, {})
|
||||
pcts.append(tag_data.get("pct", 0))
|
||||
tiers.append(tag_data.get("tier", "D"))
|
||||
|
||||
if not pcts:
|
||||
summary[tag] = {
|
||||
"avg_pct": 0,
|
||||
"top_tier": "D",
|
||||
"count_S": 0,
|
||||
"count_A": 0,
|
||||
"count_B": 0,
|
||||
"count_C": 0,
|
||||
"count_D": 0,
|
||||
}
|
||||
continue
|
||||
pcts.append(safe_pct(tag_data.get("pct")))
|
||||
tiers.append(safe_tier(tag_data.get("tier")))
|
||||
|
||||
summary[tag] = {
|
||||
"avg_pct": sum(p for p in pcts if isinstance(p, (int, float))) / max(1, len([p for p in pcts if isinstance(p, (int, float))])),
|
||||
"avg_pct": safe_avg(pcts),
|
||||
"top_tier": max(tiers, key=lambda t: "SABCD".index(t)),
|
||||
"count_S": tiers.count("S"),
|
||||
"count_A": tiers.count("A"),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,10 @@
|
|||
|
||||
from utils.safe_tag import safe_pct, safe_tier, safe_avg
|
||||
|
||||
|
||||
|
||||
from utils.safe_tag import safe_pct, safe_tier, safe_avg
|
||||
|
||||
def summarize_team_tags(team_players):
|
||||
"""
|
||||
Aggregates tag tiers + percentiles for a team.
|
||||
|
|
@ -16,23 +23,11 @@ def summarize_team_tags(team_players):
|
|||
|
||||
for p in team_players:
|
||||
tag_data = p.get(tag, {})
|
||||
pcts.append(tag_data.get("pct", 0))
|
||||
tiers.append(tag_data.get("tier", "D"))
|
||||
|
||||
if not pcts:
|
||||
summary[tag] = {
|
||||
"avg_pct": 0,
|
||||
"top_tier": "D",
|
||||
"count_S": 0,
|
||||
"count_A": 0,
|
||||
"count_B": 0,
|
||||
"count_C": 0,
|
||||
"count_D": 0,
|
||||
}
|
||||
continue
|
||||
pcts.append(safe_pct(tag_data.get("pct")))
|
||||
tiers.append(safe_tier(tag_data.get("tier")))
|
||||
|
||||
summary[tag] = {
|
||||
"avg_pct": sum(p for p in pcts if isinstance(p, (int, float))) / max(1, len([p for p in pcts if isinstance(p, (int, float))])),
|
||||
"avg_pct": safe_avg(pcts),
|
||||
"top_tier": max(tiers, key=lambda t: "SABCD".index(t)),
|
||||
"count_S": tiers.count("S"),
|
||||
"count_A": tiers.count("A"),
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ from match_engine import (
|
|||
split_two_columns,
|
||||
compute_slayer_prediction,
|
||||
rank_match_players_consistency_career,
|
||||
rank_match_players_support_specialist,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------
|
||||
|
|
@ -377,15 +378,15 @@ class DashLeagueGUI:
|
|||
p["objective_payload"] = pdata.get("objective_payload", {})
|
||||
|
||||
# DO NOT overwrite match-based DomObj
|
||||
if "objective_domination_components" not in p:
|
||||
p["objective_domination_components"] = pdata.get(
|
||||
"objective_domination_components",
|
||||
{}
|
||||
)
|
||||
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", {})
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# 4.5 Always attach match-based Domination Objective
|
||||
|
|
@ -591,6 +592,15 @@ 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)
|
||||
# ---------------------------------------------------------
|
||||
|
|
@ -708,6 +718,7 @@ Consistency Summary:
|
|||
"Domination Objective Specialist",
|
||||
"Consistency",
|
||||
"Clutch",
|
||||
"Support Specialist",
|
||||
]
|
||||
|
||||
ttk.Label(win, text="Select Tag:").pack(anchor="w", padx=10, pady=5)
|
||||
|
|
@ -744,6 +755,8 @@ Consistency Summary:
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -9,10 +9,12 @@ 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 data.league_metrics import compute_league_metrics, compute_league_clutch_metrics
|
||||
|
||||
|
||||
|
||||
def build_career_database(output_path):
|
||||
|
||||
print("USING DB:", db_access.DB_PATH)
|
||||
|
|
@ -102,6 +104,66 @@ def build_career_database(output_path):
|
|||
for pdata in all_players:
|
||||
pdata["clutch_raw"] = compute_clutch_raw(pdata)
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# NORMALIZE CAREER STAT KEYS BEFORE LEAGUE METRICS
|
||||
# ---------------------------------------------------------
|
||||
for pid, pdata in final_db["players"].items():
|
||||
career = pdata.get("career", {})
|
||||
|
||||
career["push_time"] = (
|
||||
career.get("push_time")
|
||||
or career.get("PAY_PushTime")
|
||||
or pdata.get("PAY_PushTime")
|
||||
or 0
|
||||
)
|
||||
|
||||
career["captures"] = (
|
||||
career.get("captures")
|
||||
or career.get("DOM_Captures")
|
||||
or pdata.get("DOM_Captures")
|
||||
or 0
|
||||
)
|
||||
|
||||
career["counters"] = (
|
||||
career.get("counters")
|
||||
or career.get("DOM_Counters")
|
||||
or pdata.get("DOM_Counters")
|
||||
or 0
|
||||
)
|
||||
|
||||
career["damage"] = (
|
||||
career.get("damage")
|
||||
or career.get("Damage")
|
||||
or pdata.get("Damage")
|
||||
or 0
|
||||
)
|
||||
|
||||
career["kills"] = (
|
||||
career.get("kills")
|
||||
or career.get("Kills")
|
||||
or pdata.get("Kills")
|
||||
or 0
|
||||
)
|
||||
|
||||
career["deaths"] = (
|
||||
career.get("deaths")
|
||||
or career.get("Deaths")
|
||||
or pdata.get("Deaths")
|
||||
or 0
|
||||
)
|
||||
|
||||
career["maps"] = (
|
||||
career.get("maps")
|
||||
or pdata.get("maps")
|
||||
or pdata.get("career", {}).get("maps")
|
||||
or 1
|
||||
)
|
||||
|
||||
pdata["career"] = career
|
||||
|
||||
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 4. Compute league metrics for ALL TAGS
|
||||
# ---------------------------------------------------------
|
||||
|
|
@ -113,6 +175,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.5 Build Support Specialist distribution
|
||||
# ---------------------------------------------------------
|
||||
support_distribution = []
|
||||
for pid, pdata in final_db["players"].items():
|
||||
raw_support = compute_support_specialist(pdata, league_averages)
|
||||
support_distribution.append(raw_support)
|
||||
|
||||
# Write back normalized career block
|
||||
pdata["career"] = career
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 5. Compute all tags using distributions
|
||||
# ---------------------------------------------------------
|
||||
|
|
@ -140,12 +217,45 @@ def build_career_database(output_path):
|
|||
)
|
||||
|
||||
# Domination Objective Specialist
|
||||
pdata["objective_domination_components"] = compute_dom_objective_for_career_player(
|
||||
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
|
||||
}
|
||||
|
||||
# Clutch
|
||||
pdata["clutch"] = compute_clutch(
|
||||
pdata,
|
||||
league_averages,
|
||||
clutch_distribution
|
||||
)
|
||||
|
||||
# Support Specialist
|
||||
pdata["support_specialist"] = build_support_specialist_tag(
|
||||
pdata,
|
||||
league_averages,
|
||||
support_distribution
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Consistency (Hybrid Career Tag - New System)
|
||||
# ---------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ def compute_league_metrics(all_players):
|
|||
"push_time": [],
|
||||
"headshot_rate": [],
|
||||
"pressure_eff": [],
|
||||
"deaths": [],
|
||||
"dom_actions": [],
|
||||
}
|
||||
|
||||
# -----------------------------------------
|
||||
|
|
@ -41,10 +43,11 @@ def compute_league_metrics(all_players):
|
|||
"payload": [],
|
||||
"consistency": [],
|
||||
"domination": [],
|
||||
"support_specialist": [],
|
||||
}
|
||||
|
||||
# -----------------------------------------
|
||||
# Build league averages
|
||||
# Build league averages (PER-MAP NORMALIZED)
|
||||
# -----------------------------------------
|
||||
for p in all_players:
|
||||
c = p.get("career", {})
|
||||
|
|
@ -52,13 +55,17 @@ def compute_league_metrics(all_players):
|
|||
if maps <= 0:
|
||||
continue
|
||||
|
||||
kills = c.get("kills", 0)
|
||||
deaths = c.get("deaths", 0)
|
||||
damage = c.get("damage", 0)
|
||||
# Per-map normalization
|
||||
kills = c.get("kills", 0) / maps
|
||||
deaths = c.get("deaths", 0) / maps
|
||||
damage = c.get("damage", 0) / maps
|
||||
push = c.get("push_time", 0) / maps
|
||||
captures = c.get("captures", c.get("DOM_Captures", 0)) / maps
|
||||
counters = c.get("counters", c.get("DOM_Counters", 0)) / maps
|
||||
|
||||
shots = c.get("shots", 0)
|
||||
shots_hit = c.get("shots_hit", 0)
|
||||
headshots = c.get("headshots", 0)
|
||||
push = c.get("push_time", 0)
|
||||
|
||||
KD = kills / deaths if deaths > 0 else kills
|
||||
accuracy = shots_hit / shots if shots > 0 else 0
|
||||
|
|
@ -71,6 +78,8 @@ def compute_league_metrics(all_players):
|
|||
acc["push_time"].append(push)
|
||||
acc["headshot_rate"].append(headshot_rate)
|
||||
acc["pressure_eff"].append(pressure_eff)
|
||||
acc["deaths"].append(deaths)
|
||||
acc["dom_actions"].append(captures + counters)
|
||||
|
||||
# -----------------------------------------
|
||||
# Compute league averages
|
||||
|
|
@ -103,6 +112,12 @@ def compute_league_metrics(all_players):
|
|||
# Domination
|
||||
dist["domination"].append(compute_dom_raw(p))
|
||||
|
||||
from tags.support_specialist import compute_support_specialist
|
||||
|
||||
dist["support_specialist"].append(
|
||||
compute_support_specialist(p, league_averages)
|
||||
)
|
||||
|
||||
# -----------------------------------------
|
||||
# Sort distributions
|
||||
# -----------------------------------------
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
|
||||
|
||||
import bisect
|
||||
from tags.support_specialist import build_support_specialist_tag
|
||||
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Percentile + Tier Helpers
|
||||
|
|
|
|||
|
|
@ -232,3 +232,38 @@ def compute_slayer_prediction(team_a_players, team_b_players):
|
|||
"teamA_win": round(pA * 100),
|
||||
"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
|
||||
36
rankings.py
36
rankings.py
|
|
@ -107,3 +107,39 @@ def rank_match_players_clutch(players):
|
|||
r["rank"] = i
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
ranked = []
|
||||
|
||||
for p in players:
|
||||
tag = p.get("support_specialist", {})
|
||||
raw = tag.get("raw")
|
||||
|
||||
# Skip players with no data
|
||||
if raw is None:
|
||||
continue
|
||||
|
||||
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}",
|
||||
})
|
||||
|
||||
# Sort descending by raw score
|
||||
ranked.sort(key=lambda x: x["raw"], reverse=True)
|
||||
|
||||
# Assign ranks
|
||||
for i, entry in enumerate(ranked, start=1):
|
||||
entry["rank"] = i
|
||||
|
||||
return ranked
|
||||
77
tags/support_specialist.py
Normal file
77
tags/support_specialist.py
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
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)
|
||||
82
utils/safe_tag.py
Normal file
82
utils/safe_tag.py
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import math
|
||||
|
||||
def safe_pct(value):
|
||||
"""
|
||||
Ensures pct is always a float between 0 and 100.
|
||||
None, NaN, or invalid → 0.0
|
||||
"""
|
||||
try:
|
||||
if value is None:
|
||||
return 0.0
|
||||
v = float(value)
|
||||
if math.isnan(v):
|
||||
return 0.0
|
||||
return max(0.0, min(100.0, v))
|
||||
except:
|
||||
return 0.0
|
||||
|
||||
|
||||
def safe_tier(value):
|
||||
"""
|
||||
Ensures tier is always a valid letter.
|
||||
None or empty → 'D'
|
||||
"""
|
||||
if value in (None, "", "None"):
|
||||
return "D"
|
||||
return str(value)
|
||||
|
||||
|
||||
def safe_summary(value):
|
||||
"""
|
||||
Ensures summary is always a string.
|
||||
"""
|
||||
if value is None:
|
||||
return ""
|
||||
return str(value)
|
||||
|
||||
|
||||
def safe_raw(value):
|
||||
"""
|
||||
Ensures raw is always a float.
|
||||
"""
|
||||
try:
|
||||
if value is None:
|
||||
return 0.0
|
||||
v = float(value)
|
||||
if math.isnan(v):
|
||||
return 0.0
|
||||
return v
|
||||
except:
|
||||
return 0.0
|
||||
|
||||
|
||||
def safe_tag(tag_dict):
|
||||
"""
|
||||
Normalizes an entire tag dict.
|
||||
Guarantees all fields exist and are safe.
|
||||
"""
|
||||
if not isinstance(tag_dict, dict):
|
||||
return {
|
||||
"raw": 0.0,
|
||||
"pct": 0.0,
|
||||
"tier": "D",
|
||||
"summary": ""
|
||||
}
|
||||
|
||||
return {
|
||||
"raw": safe_raw(tag_dict.get("raw")),
|
||||
"pct": safe_pct(tag_dict.get("pct")),
|
||||
"tier": safe_tier(tag_dict.get("tier")),
|
||||
"summary": safe_summary(tag_dict.get("summary"))
|
||||
}
|
||||
|
||||
|
||||
def safe_avg(values):
|
||||
"""
|
||||
Averages a list of pct values safely.
|
||||
None, NaN, invalid → treated as 0.0
|
||||
"""
|
||||
cleaned = [safe_pct(v) for v in values]
|
||||
if not cleaned:
|
||||
return 0.0
|
||||
return sum(cleaned) / len(cleaned)
|
||||
Loading…
Reference in a new issue